`operator<` 详解:JSON 值的字典序比较、类型序规则与 C++20 三路比较迁移)
JSON for Modern Cnlohmann/jsonoperator详解JSON 值的字典序比较、类型序规则与 C20 三路比较迁移【免费下载链接】jsonJSON for Modern C项目地址: https://gitcode.com/GitHub_Trending/js/jsonnlohmann/json 为basic_json提供了完整的关系运算符集合其中operator是构造小于语义、从而支撑std::sort、std::set、std::map等标准库有序容器场景的基础。本文以仓库 API 参考文档 operator_lt.md 为主线结合头文件实现与配套示例系统讲解operator的两种重载形态、跨类型与跨数值比较规则、隐藏的 JSON 类型总序type ordering、NaN/discarded的不可比较语义以及 C20 下三路比较运算符operator的替换关系。读完你将能准确预测任意两个 JSON 值的比较结果并能安全地迁移到 C20 编译环境。一、函数签名总览JSON 值之间、JSON 值与标量之间operator以非成员friend运算符形式提供共两种重载形态// until C20 bool operator(const_reference lhs, const_reference rhs) noexcept; // (1) templatetypename ScalarType bool operator(const_reference lhs, const ScalarType rhs) noexcept; // (2) templatetypename ScalarType bool operator(ScalarType lhs, const const_reference rhs) noexcept; // (2)形态 (1)比较两个 JSON 值lhs与rhs即最常用的json_a json_b形态 (2)把标量如int、double、bool、nullptr_t等转换成 JSON 值后再按形态 (1) 比较因此同时支持json 42与42 json两种书写方向。在 include/nlohmann/json.hpp 中三个重载以friend函数的形式集中声明于basic_json类内其真实行为均为先构造basic_json再比较friend bool operator(const_reference lhs, const_reference rhs) noexcept; // 宏 JSON_IMPLEMENT_OPERATOR 展开 templatetypename ScalarType, typename std::enable_if std::is_scalarScalarType::value, int::type 0 friend bool operator(const_reference lhs, ScalarType rhs) noexcept { return lhs basic_json(rhs); } templatetypename ScalarType, typename std::enable_if std::is_scalarScalarType::value, int::type 0 friend bool operator(ScalarType lhs, const_reference rhs) noexcept { return basic_json(lhs) rhs; }这里的关键词是friend因为库允许通过basic_json的类型模板参数定制容器见 ordered_map.hpp、ordered_json顶层比较运算符必须是友元才能以相同方式作用于所有特化标量重载的可用性由std::is_scalar通过 SFINAE 约束C20 分支下改用requires std::is_scalar_vScalarType。模板参数与函数参数项目说明ScalarType满足std::is_scalarScalarType::value的标量类型算术类型、枚举、指针、nullptr_t、bool等lhs(in)参与比较的第一个值rhs(in)参与比较的第二个值返回值lhs是否严格小于rhsbool异常安全No-throw guarantee函数声明为noexcept任何情况下不抛异常时间复杂度线性Linear数组按元素逐个比较、对象按条目逐个比较代价随规模线性增长二、三条核心比较规则API 参考文档把形态 (1) 的判定逻辑归纳为四条规则其中前三条决定绝大多数日常结果规则 1任一侧为discarded值直接返回false。discarded是解析器回调parser_callback_t丢弃某个值时产生的特殊内部状态不代表任何真实 JSON 数据只要lhs或rhs之一是 discarded即按不可比较处理返回false。规则 2两侧类型相同直接调用该类型的原生operator。从源码宏 JSON_IMPLEMENT_OPERATOR 可以看到每种类型的实际比较载体JSON 类型底层存储小于比较的语义arraystd::vectorarray_t逐元素字典序首个不同元素决定结果objectstd::mapobject_t默认按键排序按键排序后按键, 值对做字典序比较stringstd::stringstring_t字节级字典序booleanboolfalse truenumber_integer/number_unsigned/number_float对应整型 / 无符号 / 浮点各自原生数值比较binarystd::vectorstd::uint8_t字节序列字典序规则 3两侧类型不同时忽略取值仅按 JSON 类型总序比较类型本身。这一条是 nlohmann/json 最鲜明的设计跨类型比较的结果只取决于类型不取决于值。库为 7 大类 JSON 类型规定了固定的偏序规则上从小到大依次为nullbooleannumber整数、无符号、浮点三种子类型归并为同一档objectarraystringbinary例如任何布尔值都小于任何字符串json(true) json(any)恒为true即使布尔值是true而字符串内容是空串。在实现层这个类型总序并非依赖value_t枚举的声明顺序而是由 detail/value_t.hpp 中一张显式映射表给出的static constexpr std::arraystd::uint8_t, 9 order {{ 0 /* null */, 3 /* object */, 4 /* array */, 5 /* string */, 1 /* boolean */, 2 /* integer */, 2 /* unsigned */, 2 /* float */, 6 /* binary */ } };null0、boolean1、三种数字子类型统一为2、object3、array4、string5、binary6恰好对应上表。value_t同时提供operatorC20 前与operatorC20 起供顶层比较宏在值不可比时回退比较类型。该表注释还说明此顺序类似 Pythonb字节串可直接与字符串比较但故意让 JSONbinary不与string直接混比。规则 4补充数值自动转换。当两侧都是数字但子类型不同如一个number_integer、一个number_float时并不落到类型不同按类型序的路径而是先做数值转换再比较——这正是规则 3 中三种数字子类型归并为一档的原因。三、数值比较的细节自动转换与符号边界JSON_IMPLEMENT_OPERATOR宏把同类型比较和跨类型数值比较放在同一套代码中展开所有比较运算符、!、、、、、共用它只是传入的算子、占位结果不同。跨数值类型分支如下include/nlohmann/json.hppnumber_integervsnumber_float、number_unsignedvsnumber_float把整数值static_cast成浮点后再比较。例如17 17.0000000000001L会变成17.0 17.0000000000001。number_unsignedvsnumber_integer先检查有符号一侧是否为负——若为负则直接按负数恒小于无符号数返回固定结果1 op -1否则把非负的有符号数转成无符号再比较。宏头部注释解释了最后一点有/无符号混合比较必须先判断符号再强转否则负数转无符号会发生回绕wraparound而得出错误结论对应上游 issue #1530 的修复。这保证了对任意std::int64_t/std::uint64_t组合比较结果都与数学意义上的大小完全一致。类型相同的数字之间比较的则是原始存储值见 value_t.hpp库区分number_integer_t、number_unsigned_t、number_float_t三种数字存储。四、NaN、discarded与无序比较JSON 标准里没有NaN但库允许用户在运行时把NaN写入浮点值。NaN在数值域中无序unordered因此文档明确了三条比较结论NaN与自身比较为falseNaN与另一个NaN比较为falseNaN与任何其他数字比较为false。实现上的关键函数是 compares_unorderedstatic bool compares_unordered(const_reference lhs, const_reference rhs, bool inverse false) noexcept { if ((lhs.is_number_float() std::isnan(lhs.m_data.m_value.number_float) rhs.is_number()) || (rhs.is_number_float() std::isnan(rhs.m_data.m_value.number_float) lhs.is_number())) { return true; } return lhs.is_discarded() || rhs.is_discarded(); }宏在展开时以null_result false、unordered_result false、default_result operator(lhs_type, rhs_type)调用一旦命中NaN对数字、或任一侧 discarded宏即短路返回false否则才落到类型序比较。正因为NaN与任何数字包括它自己比较都返回false文档在std::partial_ordering语境下称NaN为不可排序这也是为什么三路比较的返回类型是std::partial_ordering含unordered而不是std::strong_ordering。补充编译期宏JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON被定义时库会启用 3.11.0 之前discarded 在奇数次求反算符中视为可排序的旧语义并为此额外重载、带JSON_HEDLEY_DEPRECATED_FOR弃用标注见 include/nlohmann/json.hpp。默认情况下对 discarded 一律返回false新旧语义一致。五、完整的可运行示例与输出解读仓库提供了最小自包含示例 operator__less.cpp#include iostream #include nlohmann/json.hpp using json nlohmann::json; int main() { // create several JSON values json array_1 {1, 2, 3}; json array_2 {1, 2, 4}; json object_1 {{A, a}, {B, b}}; json object_2 {{B, b}, {A, a}}; json number_1 17; json number_2 17.0000000000001L; json string_1 foo; json string_2 bar; // output values and comparisons std::cout std::boolalpha; std::cout array_1 array_2 (array_1 array_2) \n; std::cout object_1 object_2 (object_1 object_2) \n; std::cout number_1 number_2 (number_1 number_2) \n; std::cout string_1 string_2 (string_1 string_2) \n; }输出见 operator__less.output[1,2,3] [1,2,4] true {A:a,B:b} {A:a,B:b} false 17 17.0000000000001 true foo bar false示例源码在打印时统一用了作为分隔文案真正执行的运算符是需按(a b)的布尔值解读输出。逐条验证本文规则[1,2,3] [1,2,4]→true同类型数组按元素字典序前两个元素相等3 4成立object_1 object_2→false对象默认以std::map存储、按键有序两个对象键值内容完全相同字典序上谁也不小于谁17 17.0000000000001L→truenumber_integer与number_float跨类型但同为数字触发自动数值转换17.0 17.0000000000001foo bar→false字符串字典序比较首字符f102大于b98。示例的构建与运行方式与仓库中其他示例一致在示例文件同目录下把对应.cpp与库的单头文件 single_include/nlohmann/json.hpp 一起编译-stdc11或更高C20 下行为差异见下节例如g -stdc17 -Irepo/single_include operator__less.cpp -o operator__less。六、C20 迁移operator与重载决议版本历史明确记录两种重载均自 1.0.0 加入自 3.11.0 起在 C20 环境下被条件性地移除。原因正是 C20 的运算符重写rewritten candidates机制——三路比较运算符 operator对应文档 operator_spaceship.md被引入后a b可以由a b或反向形式自动推导。是否启用 C20 分支由宏JSON_HAS_THREE_WAY_COMPARISON控制定义于 detail/macro_scope.hpp其判定条件是编译器实现了__cpp_impl_three_way_comparison 201907L等要求。开启后头文件走 include/nlohmann/json.hpp 中的成员函数分支std::partial_ordering operator(const_reference rhs) const noexcept { const_reference lhs *this; JSON_IMPLEMENT_OPERATOR(, std::partial_ordering::equivalent, std::partial_ordering::unordered, lhs_type rhs_type) }同类型相等时返回equivalent、NaN/discarded 命中无序分支时返回unordered其余按类型序比较。由于的存在编译器会自动把lhs rhs重写为对的调用重写的候选运算符operator不再需要显式定义——这就是条件性移除的机制含义。对调用方而言只要库在 C20 下编译json_a json_b的行为语义不变NaN仍无序、discarded 仍不可比、类型总序与数值转换规则完全复用JSON_IMPLEMENT_OPERATOR宏。迁移时需注意两件事三路比较返回的是std::partial_ordering而非强排序代码中若依赖a b 0的强序假设需改为用is_eq/is_lt/is_gt或直接比较、判定若必须保留 3.10.x 时期 discarded 值的旧比较行为需要在使用前定义JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON但该路径已被标记为弃用JSON_HEDLEY_DEPRECATED_FOR指向 3.11.0不建议新代码采用。七、与其他关系运算符的关系与快速记忆operator不是孤立存在的库在同一批friend重载中提供了完整关系族include/nlohmann/json.hpp 一带声明了、!、、、、。其中operator实现为!(rhs lhs)并额外做compares_unordered短路operator、operator同理由的否定推导C20 下统一收敛到operator。因此真正承载全部排序语义的根基正是operator与operator展开用的JSON_IMPLEMENT_OPERATOR宏。理解本节的内容即可举一反三地推演其余关系运算符。快速记忆三条结论同类型用原生比较不同类型按null boolean number object array string binary的类型序比类型不比值只要出现NaN对数字或 discarded比较结果恒为false。数值跨界比较总是先做数学上正确的转换符号边界先行这是17 17.0000000000001L这类代码能得出直觉结果的根本原因。八、延伸阅读本文参考的主体文档docs/mkdocs/docs/api/basic_json/operator_lt.md顶层比较运算符与宏实现include/nlohmann/json.hppJSON_IMPLEMENT_OPERATOR宏与compares_unorderedJSON 类型总序的映射表实现include/nlohmann/detail/value_t.hppC20 三路比较开关宏include/nlohmann/detail/macro_scope.hpp配套三路比较 API 文档operator_spaceship.md可运行示例与输出operator__less.cpp、operator__less.output比较语义的单元测试跨类型、NaN、discarded 等边界用例tests/src/unit-comparison.cpp使用单文件头时对应的实现single_include/nlohmann/json.hpp【免费下载链接】jsonJSON for Modern C项目地址: https://gitcode.com/GitHub_Trending/js/json创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考