
libpqxx 自定义数据类型转换完全指南从 SQL 文本格式到 C 类型系统的深度定制【免费下载链接】ZeroTierOneA Smart Ethernet Switch for Earth项目地址: https://gitcode.com/GitHub_Trending/ze/ZeroTierOnelibpqxx 与 PostgreSQL 的通信以文本格式为主查询中的整数需要用to_string转成文本结果字段作为浮点数读取时再从文本转回 C 类型。这套转换系统覆盖面广且可扩展——你可以在自己应用的范围内教会 libpqxx 转换额外类型与 PostgreSQL 字符串格式之间的双向映射。本文以仓库中 datatypes.md 为骨架结合 strconv.hxx 与 conversions.hxx 的源码实现完整讲解type_name、nullness、string_traits及两个可选特化的用法帮助你为任意 C 类型建立可靠、高效的 PostgreSQL 文本转换。本文聚焦于 libpqxx 这一 PostgreSQL C 客户端库仓库内由 nonfree/controller/CentralDB.cpp、nonfree/controller/PostgreSQL.hpp 等控制器代码通过#include pqxx/pqxx实际使用的类型转换扩展机制。这是一项极其有用的能力但并不适合胆小的人——你需要特化若干模板而且该 API 在 libpqxx 的每次大版本发布中都有可能变化。转换机制的核心由 C 类型驱动的文本转换PostgreSQL 的文本世界观PostgreSQL 服务器接受并呈现的数据本质上是字符串形式并且对每种数据类型有自己专属的文本格式参考 strconv.hxx 中关于string conversion的定义。libpqxx 的字符串转换定义了各种 C 类型与这些 PostgreSQL 文本表示之间如何互相翻译。在应用中一次转换完全由你指定的C 类型驱动值的 SQL 类型与此无关字符串中也不存在任何能标识其类型的标记。因此如果你从数据库 SELECT 了一个 64 位整数却试图将其转换为 C 的short结果二选一要么数字足够小、放得进short直接成功要么抛出转换异常。反过来你的表里可能有一个文本列某个字段恰好长得像数字你完全可以把该值转换为整数类型甚至浮点类型——转换真正关心的只有实际的值和目标类型这两件事。推断类型与显式实例化有些情况下转换模板能从传入的参数推断出类型auto x to_string(99);另一些情况下你需要显式实例化模板auto y from_stringint(99);从源码看from_string与to_string的实现非常薄——它们只是把工作转发给string_traitsTYPE特化strconv.hxxtemplatetypename TYPE [[nodiscard]] inline TYPE from_string(std::string_view text) { return string_traitsTYPE::from_string(text); }所以一切转换能力的最终落脚点都是pqxx::string_traits的特化。只有最朴素的转换被支持十六进制、八进制、多余的正负号、科学计数法这些花哨特性统统不可用空白也不会被剥除——只有 PostgreSQL 与to_string()输出的那类字符串才能被转换strconv.hxx。from_string还提供了一个按引用赋值、可推断类型的重载from_string(text, value)它在 strconv.hxx 中定义为value from_stringT(text);。支持一个新类型需要做什么假设你有一个自定义 SQL 类型希望能在数据库中存取它。有时你并不需要完整的支持——比如你可能只需要转成字符串而不需要从字符串转回来。转换在编译期定义所以不必害怕不完整漏掉某一步不会在运行期崩溃、也不会弄坏数据最坏的结果只是代码编不过。一个完整的转换需要以下要素对应 strconv.hxx 中定义的三个核心模板一个 C 类型——可以是自定义的也可以是第三方库的甚至是标准库中 libpqxx 尚未支持的类型转换逻辑围绕任何类型都可以构建类型本身不需要包含任何特殊方法或成员这正是 libpqxx 能支持int这类内建类型的原因特化pqxx::type_name——指定该类型的人类可读名称凡是错误消息等以文本形式提及你类型的地方都需要它特化pqxx::nullness——描述你的类型是否内置了null 值以及如何产生、识别它特化pqxx::string_traits——在这里定义真正的转换。下面按步骤逐一展开。第一步你的类型由于 C 类型决定了正确的转换你需要一个尚未定义转换的类型。规则是一个类型一套转换。该类型不必由你创建——转换逻辑被设计成可以围绕任何类型构建所以你完全可以为定义在别处的类型建立转换就像 libpqxx 为int等内建类型所做的那样。枚举类型有捷径如果目标类型是一个 enum你什么都不用做只需在翻译单元顶部的全局命名空间中调用预处理器宏PQXX_DECLARE_ENUM_CONVERSION并传入类型名即可。查看该宏在 strconv.hxx 中的定义#define PQXX_DECLARE_ENUM_CONVERSION(ENUM) \ template struct string_traitsENUM : pqxx::internal::enum_traitsENUM \ {}; \ template inline std::string const type_nameENUM { #ENUM }它会同时特化string_traits派生自pqxx::internal::enum_traitsENUM后者在 strconv.hxx 中把枚举值当作底层整型进行数值转换和type_name。官方示例注释于 strconv.hxx#include iostream #include pqxx/strconv enum X { xa, xb }; namespace pqxx { PQXX_DECLARE_ENUM_CONVERSION(x); } int main() { std::cout pqxx::to_string(xa) std::endl; }注意宏使用位置在pqxx命名空间内而枚举本身定义在全局命名空间。另外nullness对枚举有一套自动的 SFINAE 特化templatetypename ENUM struct nullnessENUM, std::enable_if_tstd::is_enum_vENUM : no_nullENUMstrconv.hxx即枚举默认没有内建 null 值。如果你需要比宏更细的控制可以手动使用enum_traits。第二步特化type_name转换出错时libpqxx 会为用户拼装错误消息其中有时会包含被转换类型的名字。为此存在一个模板变量pqxx::type_name对任意类型T其特化应提供T的人类可读名称namespace pqxx { template std::string const type_nameT{T}; }是的这意味着你需要在 pqxx 命名空间内定义一些东西。未来的 libpqxx 版本可能把这个移入独立命名空间。请在翻译单元中尽早定义它——早于任何可能触发 libpqxx 需要该名字的代码这样需要类型名的 libpqxx 代码才能看到你的定义。默认的type_name特化使用std::type_info::name()并在 gcc 类编译器上尝试 demanglestrconv.hxx而 generic_into_buf 的溢出错误消息正是通过type_nameT拼接类型名的conversions.hxx。第三步特化nullness结构体模板pqxx::nullness定义你的类型是否内置了null 值如果有还提供产生与识别 null 值的成员函数。其默认定义strconv.hxx包含四个成员has_null、always_null、is_null(TYPE const )与null()。最简单也最常见的情况大多数类型没有内建 null 值此时让你的 nullness traits 从pqxx::no_null派生namespace pqxx { template struct nullnessT : pqxx::no_nullT {}; }no_null定义了has_null false、always_null false且is_null恒返回falsestrconv.hxx。注释中特别提醒像int或std::string这类类型没有内建 null若想为它们表示 SQL null就得包一层有 null 值的东西例如std::optionalint表达要么是一个 int要么是 null。如果你的类型确实有自然的 null 值定义会稍微复杂一些namespace pqxx { template struct nullnessT { static constexpr bool has_null{true}; static constexpr bool always_null{false}; static bool is_null(T const value) { // Return whether value is null. return ...; } [[nodiscard]] static T null() { // Return a null value. return ...; } }; }你可能会问既然有产生 null 值的函数为什么还要一个检查是否为 null 的函数为什么不直接把值和null()的结果比较因为两个 null 值可能不相等T可能有多个不同的 null 值也可能重载了比较运算符类似 SQL 中 NULL 不等于 NULL 的语义。这正是 strconv.hxx 注释强调的不要在泛型代码中拿null()的结果去比较判空的原因。第三种情况你的类型可能始终代表 null 值例如std::nullptr_t和std::nullopt_t。此时把nullnessTYPE::always_null置为true当然has_null也是true并且不需要定义任何实际的转换。第四步特化string_traits这是工作量最大的部分对于始终为 null的类型可以跳过但那种情况很少见。特化pqxx::string_traits模板namespace pqxx { template struct string_traitsT { static T from_string(std::string_view text); static zview to_buf(char *begin, char *end, T const value); static char *into_buf(char *begin, char *end, T const value); static std::size_t size_buffer(T const value) noexcept; }; }你还需要编写这些成员函数——或者尽可能多地编写直到代码能编过为止。下面的小节逐一剖析这四个函数。from_string从文本解析值from_string把字符串解析为T的值并返回。输入串不保证以零结尾——它就是从开头到结尾不含的string_view。测试时务必覆盖字符串不以零字节结尾的情况字符串完全可能不是一个合法的T值此时抛出pqxx::conversion_error。它继承自std::domain_errorexcept.hxx当然也可能遇到其他错误抛其他异常也完全可以但当这显然不是T的正确格式时请抛conversion_error。to_buf把值转成服务器能理解的字符串to_buf把T的值转换为 PostgreSQL 服务器能理解的字符串。调用方会给你一个可写缓冲区从begin到end不含。这是半开区间所以不要访问*end。如果缓冲区不足以完成转换抛出pqxx::conversion_overrun它是conversion_error的子类定义见 except.hxx。判定不必精确——你可以稍微悲观一点要求比实际需要更多的空间但只要有溢出风险就一定要抛出异常。你不必须使用这个缓冲区。例如pqxx::string_traitsbool::to_buf返回一个编译期常量字符串完全无视缓冲区conversions.hxx 附近。即使你使用缓冲区字符串也不必从缓冲区开头写起——比如整数转换先从缓冲区末尾写最低位数字再往前写更高位只是因为它更方便对应 conversions.hxx 中integral_traits的实现思路。返回值类型是pqxx::zview——本质上是一个std::string_view但有一个额外保证在string_view之后紧跟一个有效的零字节。零字节不计入 size但它一定存在。用代码表达这条规则必须成立void invariant(zview z) { assert(z[std::size(z)] 0); }务必在end之前写入结尾零。如果结尾零放不进缓冲区那就说明空间根本不够完成转换。当心 locale转换时如果使用sprintf等标准库特性它们会遵守系统当前 locale——同一个整数 1000000 在你的系统上是 1000000在别人那里可能是 1,000,000 或 1.000.000在印度系统上甚至可能是 1,00,000。进出来自数据库的值必须使用非本地化格式。请一律使用 libpqxx 的转换函数pqxx::from_string、pqxx::to_string、pqxx::to_buf。这也呼应 strconv.hxx 中对to_string的说明不做特殊格式化、忽略任何 locale 设置。into_buf更严格的to_buf这是to_buf的更严格版本。所有要求相同但额外要求必须把字符串写入提供的缓冲区并且恰好从begin开始。因此该函数只返回一个普通指针紧跟结尾零之后的地址。调用方若想使用字符串在begin处即可找到若想往缓冲区剩余部分写别的值可以从你返回的位置开始。当你的to_buf没有特殊优化技巧、只是把文本从缓冲区开头写起时可以借助现成的 generic_to_buf它调用into_buf并把返回指针减 1 作为zview的长度减去结尾零还会对 null 值返回空视图。size_buffer估算所需缓冲区大小在这里估算把T转成字符串所需的缓冲区空间。能精确就精确但必须精确时就悲观一些——浪费几个字节通常比花大量时间计算精确空间更好而因为低估缓冲区导致转换失败则是最糟的结果。注意两点缓冲区大小要包含结尾零如果你的to_buf需要的空间超出存储结果的最小需求也要一并计入尽量把size_buffer做成constexpr——它能让调用方在编译期确定大小、在栈上分配缓冲区。源码中integral_traits::size_buffer正是constexprconversions.hxx其公式为符号位如有 类型能可靠表示的十进制位数 1 个只能部分表示的额外十进制位 结尾零。size_buffer还支持可变参数版本一次性估算多个值的总空间strconv.hxx配合可变参数to_bufstrconv.hxx可在循环中批量高效转换。可选优化一特化is_unquoted_safe把数组或复合类型转成字符串时libpqxx 可能需要给值加引号并转义特殊字符这很耗时。但某些类型——比如整型、浮点型——的字符串表示永远不可能包含引号、逗号、反斜杠这类特殊字符。此时数组/复合类型中就不需要对这些值加引号或转义。如果你的类型属于此类可以通过如下定义告诉 libpqxxnamespace pqxx { template inline constexpr bool is_unquoted_safeMY_TYPE{true}; }它在 strconv.hxx 中默认定义为false宁可慢一点也要正确在 conversions.hxx 中对内建整型、浮点型、bool等均置为true。从数组序列化实现 array_string_traits::into_buf 可以看到它在运行时的收益当元素类型is_unquoted_safe为 true 时直接into_buf写值、跳过加引号与转义分支否则要为每个元素加双引号并转义\与。size_buffer也同样分叉安全类型按元素原始大小累加3 Σ(size_buffer(elt) - 1)不安全类型则按最坏情况每个元素双倍大小加引号估算conversions.hxx。这个特化完全可以省略——它纯粹是当你完全确定安全时的性能优化。不要在以下情况下开启你的类型的字符串表示可能包含逗号、分号、括号、花括号、引号、反斜杠、换行或其他任何需要转义的字符。值得一提的是is_unquoted_safe对包装模板具有传递性std::optionalT特化为is_unquoted_safeTconversions.hxxstd::unique_ptrT、std::shared_ptrT、std::variantT...取各类型与运算同理conversions.hxx。可选专题特化param_format二进制参数这一项通常不需要操心——除非你正在编写一个表示原始二进制数据的类型或编写一个某些特化可能包含原始二进制数据的模板。当你调用参数化语句或带参数的预编译语句时libpqxx 需要把参数传给底层的 C 级 PostgreSQL 客户端库 libpq。传参有两种格式文本格式所有值都以字符串表示服务器再转换成自己的内部二进制表示。这正是字符串转换的全部意义也是绝大多数参数类型所走的路径二进制格式当参数是一段连续的原始字节、对应 SQL 类型为BYTEA时libpqxx 为了效率绕过文本格式——服务器可以直接使用这份二进制数据无需任何转换或额外处理传输时体积也只有一半。人们有时会问为什么不能把所有类型都按二进制处理因为一般情况并不那么明确二进制格式没有文档不保证跨平台、也不保证长期稳定还缺少可靠的格式搞错了检测手段而且转换也未必像听上去那么直接高效。所以对一般情况libpqxx 坚持使用文本格式只有原始二进制数据是明确稳赢的例外。简而言之传参机制需要知道这个参数是不是二进制字符串正常情况下它假定不是文本格式永远是安全的选择只是尽量在更快的地方用二进制。param_format函数模板就是做这个决定的strconv.hxx默认对所有类型返回format::texttemplatetypename TYPE inline constexpr format param_format(TYPE const ) { return format::text; }只有可能是二进制字符串的类型才需要特化它。查看 conversions.hxxstd::basic_string_viewstd::byte的param_format返回format::binary。可能是二进制你或许认为一个类型是不是二进制是确定的但泛型类型有些复杂std::shared_ptr、std::optional等模板是另一种类型的包装器std::optionalT在T是二进制时才是二进制否则不是。如果你在为这类包装模板建立支持很可能需要为它实现param_format——conversions.hxx 中std::optionalT的特化正是把决定转发给param_format(*value)决定基于给定的对象而非类型本身看std::variant——如果它既能持有int又能持有二进制字符串那么它算不算二进制参数不知道具体对象就无法决定。源码对此的做法是对std::variant使用std::visit将决定转发给当前持有的类型conversions.hxx容器是另一个难点std::vectorT该不该按二进制传参即使T是二进制类型目前也没有办法以二进制格式传递数组所以容器始终按文本传递。开箱即用的类型支持与包装类型为帮助你判断哪些类型还需要自己特化以下是从 conversions.hxx 中确认的内建支持清单全部位于pqxx命名空间类别已特化的类型整型short、unsigned short、int、unsigned、long、unsigned long、long long、unsigned long long均派生自internal::integral_traits见 L190-L219浮点float、double、long double派生自internal::float_traits见 L220-L229布尔boolL232 起to_buf返回编译期常量字符串字符char、signed char、unsigned charL256-L276字符串类std::string、std::string_view、zview、std::stringstreamL574-L692裸指针char const *、char *L465-L520指针可空故 nullness 与裸类型不同C 数组char[N]L540-L548智能指针std::unique_ptrT, Args...、std::shared_ptrTL730-L828自动继承T的转换与is_unquoted_safe可选与变体std::optionalTL284-L333has_null true、std::variantT...L336-L399注意from_string被 delete、null()被删除恒空类型std::nullptr_t、std::nullopt_t、std::monostateL408-L462always_null语义二进制数据std::basic_stringstd::byte、std::basic_string_viewstd::byteL832-L963param_format返回format::binarySQL 数组std::vectorT, Args...、std::arrayT, NL1074-L1110由array_string_traits实现注意尚不支持用from_string解析数组类型因为那需要连接引用见 L1066-L1068这印证了文档中的承诺如果你有T的转换那么std::optionalT、std::shared_ptrT、std::unique_ptrT的转换也会自动就绪。而std::chrono::year_month_day的转换特化则位于 time.hxx以 ISO-8601 格式的格雷果里日期表示BC 年份支持仍标注为实验性。另有一个值得了解的细节char及其有符号/无符号变体没有字符串转换——这类转换存在危险的歧义应当把它当文本还是小整数。源码故意用disallowed_ambiguous_char_conversion把它们全部 delete让编译器报错信息包含歧义根源的提示conversions.hxx。完整示例为一个自定义类型搭建全套转换综合以上各步骤一个带自然 null 值、需要转义、普通文本参数的自定义类型的完整骨架如下参照文档 datatypes.md 的四个步骤顺序#include pqxx/pqxx // 1) 自定义类型不需要包含任何特殊成员 struct my_value { int number; bool valid; // valid false 表示 null }; // 2) 在 pqxx 命名空间中尽早定义 type_name namespace pqxx { template std::string const type_namemy_value{my_value}; } // 3) nullness该类型有内建 null 值且 null 值之间可能不相等 namespace pqxx { template struct nullnessmy_value { static constexpr bool has_null{true}; static constexpr bool always_null{false}; static bool is_null(my_value const value) { return not value.valid; } [[nodiscard]] static my_value null() { return my_value{0, false}; } }; } // 4) string_traits真正的转换逻辑 namespace pqxx { template struct string_traitsmy_value { static my_value from_string(std::string_view text) { if (text NULL) return my_value{0, false}; // 格式不正确时抛 conversion_error try { return my_value{from_stringint(text), true}; } catch (conversion_error const ) { throw conversion_error{Invalid my_value: std::string(text)}; } } static zview to_buf(char *begin, char *end, my_value const value) { if (is_null(value)) return generic_to_buf(begin, end, NULL); // 按文本处理 return string_traitsint::to_buf(begin, end, value.number); } static char *into_buf(char *begin, char *end, my_value const value) { return string_traitsint::into_buf(begin, end, value.number); } static std::size_t size_buffer(my_value const value) noexcept { return is_null(value) ? 5 : string_traitsint::size_buffer(value.number); } }; }要点回顾尽早定义type_name让错误消息正确显示你的类型名nullness 决定 SQL NULL 的映射——is_null与null()分开提供因为多个 null 值不必相等to_buf与into_buf的契约半开区间[begin, end)、zview 保证末尾零字节、缓冲区不足抛conversion_overrun不要使用依赖 locale 的标准库格式化函数统一走 libpqxx 的转换 API只实现当前编译所需的函数——转换在编译期定义缺一部分也只是编不过而已。结语libpqxx 的类型转换系统把PostgreSQL 文本协议与C 类型系统用一组清晰、可特化的模板桥接起来type_name提供类型的人类可读名称nullness描述 SQL NULL 的映射string_traits承载真正的双向转换is_unquoted_safe与param_format则在数组/复合类型与 BYTEA 二进制参数两个特定场景下提供性能优化。由于转换完全由 C 类型驱动只要你的类型有了一套正确的特化它就自动获得了to_string/from_string支持、结果字段读取支持、参数绑定支持——这正是 ZeroTier 中央控制器通过 CentralDB.cpp、PostgreSQL.hpp 等代码使用 libpqxx 存取数据库的底层基础。需要再次强调的是该 API 在 libpqxx 的每个大版本都可能变化实现自定义转换前请以当前版本仓库中的 strconv.hxx 与 conversions.hxx 为准。【免费下载链接】ZeroTierOneA Smart Ethernet Switch for Earth项目地址: https://gitcode.com/GitHub_Trending/ze/ZeroTierOne创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考