首页
/ 深入解析 nlohmann::basic_json::operator>:JSON for Modern C++ 的"大于"比较语义与底层实现

深入解析 nlohmann::basic_json::operator>:JSON for Modern C++ 的"大于"比较语义与底层实现

2026-09-07 20:09:48作者:何举烈Damon

导读

operator> 是 JSON for Modern C++(nlohmann::json)为 basic_json 提供的关系比较运算符之一,用于判断一个 JSON 值与另一个 JSON 值(或任意 C++ 标量)之间是否存在严格的"大于"关系。本文围绕 operator_gt.md 官方 API 文档展开,结合单头文件实现、相关运算符文档与单元测试,系统讲解其三重载签名、跨类型比较规则、NaN/discarded 等无序值边界语义、C++20 三路比较改写后的行为变化,并通过可编译示例验证实际用法。读完本文,你将能准确预判任意两个 JSON 值执行 > 的结果,理解为何"比较大小"在 JSON 类型体系下需要特殊约定,以及如何规避常见的误用场景。

函数签名:直到 C++20 的三组重载

该运算符属于 nlohmann::basic_json 命名空间的自由函数(friend),以 const_reference(即 const basic_json&)接收操作数。文档给出三个重载:

// until C++20
bool operator>(const_reference lhs, const_reference rhs) noexcept;   // (1)

template<typename ScalarType>
bool operator>(const_reference lhs, const ScalarType rhs) noexcept;  // (2)

template<typename ScalarType>
bool operator>(ScalarType lhs, const const_reference rhs) noexcept;  // (2)
  • 重载 (1):比较两个 JSON 值 lhsrhs,按下文"比较规则"判定。
  • 重载 (2):比较"一个 JSON 值与一个标量"或"一个标量与一个 JSON 值"。两个方向的模板版本共享同一语义:先把标量转换为 JSON 值,再套用规则 (1)。例如 json(17) > 1616 > json(17) 都可用,但结果相反。
  • 所有重载都标记为 noexcept,即不抛出任何异常。
  • 声明中的注释 "until C++20" 提示:从 3.11.0 版本起,若编译环境启用了 C++20 三路比较,该自由函数版本的运算符将被条件性地移除(详见后文"C++20 影响"一节)。

参数、模板约束与返回值

文档对各要素的约定如下:

要素 约定
模板参数 ScalarType 必须是满足 std::is_scalar<ScalarType>::value 的标量类型
lhs(in) 参与比较的第一个值
rhs(in) 参与比较的第二个值
返回值 lhs 是否严格大于 rhs,即 #!cpp bool
异常安全 No-throw guarantee:函数永不抛出异常
复杂度 线性(Linear,与参与比较的数组/对象元素规模相关)

比较规则:以 !(lhs <= rhs) 为核心的取值语义

规则 (1) 的官方定义可以拆成两层:

  1. 无序短路:如果出现下列任一情况,比较结果恒为 false
    • (1) 任一操作数是 discarded 类型(由带回调的解析丢弃节点产生,对应 value_t::discarded);
    • (2) 任一操作数是 NaN,且另一个操作数是 NaN 或任意数字。
  2. 否则,返回 #!cpp !(lhs <= rhs)——即把 > 定义为 operator<= 的逻辑取反。

这条"双非(double inverse)"定义值得展开:由于 <= 内部又定义为 !(rhs < lhs)> 实际上是对 < 做了两次取反。源码中 operator> 的实现与此完全对应,位于 include/nlohmann/json.hpp

friend bool operator>(const_reference lhs, const_reference rhs) noexcept
{
    // double inverse
    if (compares_unordered(lhs, rhs))
    {
        return false;
    }
    return !(lhs <= rhs);
}

可以看到实现先调用 compares_unordered 做无序性判定,命中时直接返回 false,绝不进入后续比较,这是保证 NaN/discarded 不被"当作可排序值"参与运算的关键防线。

运算符等价关系速查

结合六个关系运算符的闭包关系,下表便于记忆(u 表示无序、恒假场景):

表达式 语义推导
lhs > rhs 无序则 false,否则 !(lhs <= rhs)
lhs <= rhs 无序则 false,否则 !(rhs < lhs)
lhs >= rhs 无序则 false,否则 !(lhs < rhs)
lhs < rhs JSON_IMPLEMENT_OPERATOR 宏逐类型实现

也就是说,除 ==/!= 外的四个偏序运算符共享同一套"同型逐元素 / 跨数值类型转换 / 跨类型按序 / 无序短路"的分支逻辑,只是组合方式不同。

同型 vs 跨类型:底层比较分支宏

> 的真正比较动作发生在 JSON_IMPLEMENT_OPERATOR 宏展开处(见 include/nlohmann/json.hpp),其分支策略为:

  • 类型相同:按 value_t 分派到对应底层容器或标量:
    • array → 底层 array_t(默认 std::vector)的 operator>
    • object → 底层 object_t(默认 std::map)的 operator>
    • string → 底层 string_t(默认 std::string)的字典序比较
    • boolean → 直接比较布尔值
    • number_integer / number_unsigned / number_float → 数字间直接比较
    • binary → 底层 byte_container_with_subtype 容器比较
    • null → 返回预置结果(null == null 为真,其它关系同型下由各自规则决定)
    • discardeddefault → 返回 unordered_result(即 false
  • 数值跨类型integer ↔ floatunsigned ↔ float 通过 static_cast<number_float_t> 统一后比较;integer ↔ unsigned 先判断是否有负数,负数参与比较时依据符号映射,避免有符号/无符号混比出错。
  • 非同型、非数值:若判断为无序(见下节)返回 false,否则落入按类型序的默认结果——这正是不同 JSON 类型之间(如 "foo"42)能够稳定给出可比结果的原因。

因此 operator> 的"线性复杂度"来自数组/对象元素级递归比较或字符串逐字符比较;跨类型时通常为 O(1)。

边界语义:NaNdiscarded 为何"永不大"

文档用两个专门 note 强调了无序值(unordered values)的处理:

NaN 在数字域内是无序的,以下比较全部为 false

  1. NaN 与自身比较;
  2. NaN 与另一个 NaN 比较;
  3. NaN 与任意其它数字比较。

注意这与 IEEE-754 浮点常识一致:NaN > NaN 在原生 double 上也是 false。但库层面需要显式拦截,因为"JSON 数字内部存的是 double,而 NaN 可由用户直接构造/解析进 number_float",若不加拦截,!(NaN <= 1.0) 会得到 true,从而让 NaN > 1.0 错误地为真。

实现上,拦截逻辑集中在 include/nlohmann/json.hppcompares_unordered

static 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();
}

它覆盖两种无序情形:其一为 number_floatNaN 且对方是任意数字;其二为任一操作数是 discarded(通过 json::parse 的回调函数丢弃节点后可产生此类值)。

可选的旧版行为开关

源码中还保留了一个编译期宏 JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON:开启后,discarded 值若参与的是"由其它运算符奇数次取反而来"的比较(即 > 恰好属于此类,因为它基于 <=/< 取反),则会按旧版规则放宽为有序,见上述代码中

#if JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON
    return (lhs.is_discarded() || rhs.is_discarded()) && !inverse;
#else
    return lhs.is_discarded() || rhs.is_discarded();
#endif

默认(不定义该宏)行为即文档所述:只要涉及 discarded 就视为无序并返回 false。该宏在 3.11.0 起被标记弃用(include/nlohmann/json.hpp),仅供需要兼容旧行为的老项目在编译期回退使用。

C++20 影响:三路比较与"重写候选"

文档在 overload resolution note 中提示:自 C++20 起,重载决议会考虑由 operator<=> 自动生成的"重写候选"(rewritten candidate)。也就是说,即使你在源码中写的是 a > b,编译器也可能把它改写为对 a <=> b 结果做关系判断,而不必调用本文的自由函数版本。

触发条件取决于宏 JSON_HAS_THREE_WAY_COMPARISON,其判定位于 include/nlohmann/detail/macro_scope.hpp:当同时定义了 __cpp_impl_three_way_comparison >= 201907L__cpp_lib_three_way_comparison >= 201907L(即具备完整 C++20 三路比较的编译器和标准库)时置 1,否则置 0。启用后 basic_json 以成员函数形式提供

std::partial_ordering operator<=>(const_reference rhs) const noexcept
std::partial_ordering operator<=>(ScalarType rhs) const noexcept   // 要求 ScalarType 为标量

参见 include/nlohmann/json.hpppartial_ordering 正是为 NaN 场景选型的:NaN 参与的三路比较返回 std::partial_ordering::unordered,由此 a > b 自动归为 false,与本文档描述的"无序则假"语义无缝衔接。

这也解释了版本历史条目:"1. Added in version 1.0.0. Conditionally removed since C++20 in version 3.11.0."——自 3.11.0 起,在满足 JSON_HAS_THREE_WAY_COMPARISON 的 C++20 构建中,独立的自由函数版 operator> 被移除,转由 <=> 的合成比较接管;C++11/C++14/C++17 构建则继续使用本文档描述的实现。

完整示例与运行结果

官方文档在 ??? example 折叠块中给出了覆盖数组、对象、数字、字符串四类值的演示,源文件位于 examples/operator__greater.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';
}

对应的预期输出见 examples/operator__greater.output

[1,2,3] > [1,2,4] false
{"A":"a","B":"b"} > {"A":"a","B":"b"} false
17 > 17.0000000000001 false
"foo" > "bar" true

解读四个输出可同时检验上述全部规则:

  1. [1,2,3] > [1,2,4]false:数组比较按元素字典序进行,前两个元素相等,第三个 3 < 4,故整体小于对方。
  2. {"A":"a","B":"b"} > {"B":"b","A":"a"}false:默认 object_tstd::map,键天然有序,且该库将对象相等性定义为"键值对内容相等、与书写顺序无关",因此两者相等,严格大于不成立。
  3. 17 > 17.0000000000001false:数值跨 integer/float 比较时统一转 double 后按数值大小比,17 严格小于右值。
  4. "foo" > "bar"true:字符串按字典序逐字符比较,'f' > 'b'

如需亲自编译,在 C++11/C++14/C++17 环境下直接包含单头文件版本即可:#include <nlohmann/json.hpp>(对应 single_include/nlohmann/json.hpp,其中的 friend operator> 实现位于约 L25331 起的比较运算符区),例如 g++ -std=c++17 operator__greater.cpp && ./a.out;若用 C++20 编译则会走 <=> 合成路径,输出保持一致。

测试侧的交叉验证

单元测试 tests/src/unit-comparison.cpp 对全类型组合的关系闭包做了穷举式校验,其中针对 > 的关键断言是:

CHECK((j_values[i] > j_values[j]) == !(j_values[i] <= j_values[j]));
CHECK((j_values[i] > j_values[j]) == !!(j_values[j] < j_values[i]));

第一行直接验证文档核心公式 operator> == !(operator<=);第二行验证 a > bb < a 严格互斥(排除"既大于又小于"与"同时不小于不小于"的错乱)。测试构造的 j_values 覆盖了 null、布尔、整数、无符号整数、浮点(含精度边界用例如 max_uint64above_int64_max,见该文件 L270-L315)、字符串、数组与对象等全部 value_t,并与版本号段 JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON 分组执行,确保新旧两套 discarded 语义下 > 都满足自洽性。

另外在 C++20 分支中,测试还会断言 (j_values[i] <=> j_values[j]) 与预期矩阵完全一致(tests/src/unit-comparison.cpp),从另一侧保证 <=> 派生出的 > 与旧版自由函数结果等价。

相关 API 与版本演进小结

  • 若需要对称的"小于等于"语义,请参考 operator<=> 的实现直接建立在其取反之上)。
  • 三路比较版本参见 operator<=>,C++20 下它取代了本文的运算符成为实际比较入口。
  • 同一比较族的其余运算符(<<=>===!=)在 include/nlohmann/json.hpp 中相邻定义,共享 compares_unordered 与比较宏。

版本演进要点:operator> 自 1.0.0 起提供标量与双值两类重载;3.11.0 起在完整支持 C++20 三路比较的环境中被条件性移除,改为由 operator<=> 合成。在使用时应牢记两条黄金法则:涉及 NaN 或解析丢弃值(discarded)的任何 > 比较结果都是 false不同 JSON 类型的比较遵循库内约定的类型次序与逐元素/字典序规则,而非直觉上的"数字与字符串谁大"——若需要类型安全判断,先配合 is_number()is_string() 等类型谓词做显式分流会更为稳妥。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.76 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
858
1.35 K
docsdocs
暂无描述
Markdown
899
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
923
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.83 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
532
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.03 K
524
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
393