首页
/ JSON for Modern C++ 之 basic_json::operator==:相等性比较的完整语义与源码解析

JSON for Modern C++ 之 basic_json::operator==:相等性比较的完整语义与源码解析

2026-09-07 16:02:29作者:幸俭卉

本篇技术指南以 JSON for Modern C++(nlohmann/json)的官方 API 文档 docs/mkdocs/docs/api/basic_json/operator_eq.md 为骨架,系统讲解 basic_json 相等性比较运算符 == 的完整语义:从函数签名、比较规则、NaN/null/discarded 等特殊值行为,到浮点数精度陷阱与不同 basic_json 特化(jsonordered_json)之间的差异,并结合 include/nlohmann/json.hpp 的真实实现与 tests/src/unit-comparison.cpp 测试逐层剖析。读完你将掌握如何正确、可靠地在自己的 C++ 工程中比较 JSON 值,并能在需要近似比较浮点数或自定义语义时写出安全的替代方案。

函数签名与 C++ 版本差异

// 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)

// since C++20
class basic_json {
    bool operator==(const_reference rhs) const noexcept;              // (1)

    template<typename ScalarType>
    bool operator==(ScalarType rhs) const noexcept;                   // (2)
};

两个重载的用途:

  1. JSON 与 JSON 的比较:比较两个 JSON 值是否相等。
  2. JSON 与标量的比较:把标量转换为 JSON 值后再按规则 1 比较。该重载对任意满足 std::is_scalar<ScalarType>::value 的类型生效(C++20 后约束为 std::is_scalar_v<ScalarType>),例如整数、浮点数、布尔值、指针与 nullptr。两个方向(JSON == 标量、标量 == JSON)都被支持。

从源码看,C++20 分支通过 requires 约束(见 include/nlohmann/json.hpp),C++17 及更早版本则通过 std::enable_if 在模板参数上做 SFINAE 约束(include/nlohmann/json.hpp)。运算符从 free function 变为 member function 并不改变比较语义,只是让重写规则(rewritten candidates)与 <=> 联动更顺畅。

需要指出:在采用 three-way comparison(<=>)的构建里,operator!= 可由 == 自动重写派生,无需显式声明(源码中仅在 #else 分支显式给出 operator!= 定义,见 include/nlohmann/json.hpp)。

比较规则

operator==(重载 1)按下列规则判定两个 JSON 值相等:

  • 两个 JSON 值相等,当且仅当:(1) 两者都不是 discarded 值;(2) 两者类型相同,且其存储值按照各自底层容器/类型的 operator== 判定相等。
  • 整数与浮点数在比较前会自动转换。例如 17(整数)与 17.0(浮点数)相等。

类型相同时的底层比较

当两侧类型一致时,比较会被分派到对应底层存储上。源码中的 JSON_IMPLEMENT_OPERATOR 宏(include/nlohmann/json.hpp)用一个 switch(lhs_type) 覆盖全部 value_t 枚举:

value_t 实际比较对象
array 底层 array_t(默认 std::vector<basic_json>)的 operator==
object 底层 object_t(默认 std::map,见下方特化说明)的 operator==
string 底层 string_t(默认 std::string)的 operator==
boolean 底层 boolean_tbool)的比较
number_integer / number_unsigned / number_float 对应数值类型的比较(浮点默认是 double,即 json::number_float_t
binary 底层 binary_tbyte_container_with_subtype)的比较
null 恒定返回比较为真(同一类型 null 彼此相等)
discarded / 默认 落入"无序"分支,比较结果不成立

跨数值类型的自动转换

当两侧是不同数值类型时,宏在进入 switch 之前会先做转换(include/nlohmann/json.hpp):

  • number_integer vs number_floatnumber_unsigned vs number_float:整数值被 static_cast<number_float_t> 提升为浮点后比较;
  • number_unsigned vs number_integer(及反向):先检查 number_integer 一方是否为负。若为负则直接按不相等处理;否则统一提升为 number_unsigned 后比较。这避免了无符号与有符号混比时的隐式转换陷阱。

因此 json(17) == json(17.000000000000001L)json(1u) == json(1) 这类"字面不同、数值语义等价"的表达式都会返回 true,而负整数永远不会被错误地当作巨大的无符号数。

模板参数与函数参数

  • ScalarType:按 std::is_scalar<ScalarType>::value(C++20 为 std::is_scalar_v<ScalarType>)约束的标量类型,包括算术类型、枚举、指针、成员指针与 nullptr_t
  • lhs (in):参与比较的第一个 JSON 值。
  • rhs (in):参与比较的第二个值(JSON 值或将被转换为 JSON 值的标量)。
  • 标量重载的实现本质为 *this == basic_json(rhs)(或反向 basic_json(lhs) == rhs),见 include/nlohmann/json.hpp

返回值、异常安全与复杂度

  • 返回值bool,表示 lhs / *thisrhs 是否相等。
  • 异常安全无异常保证(no-throw guarantee)noexcept 声明,此函数从不抛出异常。即便值内部是数组或对象,其比较所依赖的容器比较操作也要求不抛异常(GCC 下甚至显式忽略 -Wfloat-equal 警告,见 include/nlohmann/json.hpp)。
  • 复杂度线性(Linear)。最坏情况下需要比较容器中每个元素,实际取决于两侧 JSON 的规模与形态。

特殊值的比较语义(重点注意事项)

文档对三类容易踩坑的特殊值给出了明确约定,这在 include/nlohmann/json.hppcompares_unordered 实现中有对应逻辑支撑:

1. NaN 在数值域内是"无序"的,以下比较全部返回 false

  • 一个 NaN 与它自身比较;
  • 一个 NaN 与另一个 NaN 比较;
  • 一个 NaN 与任何其他数字比较。

这是 IEEE 754 浮点语义的直接体现。compares_unordered 会检测"某一方是浮点 NaN 且另一方是任意数值类型"的组合并提前判定为不相等。

2. JSON null 值彼此全部相等。

因为宏对 value_t::null 直接返回 truenull_result 参数),所以 json(nullptr)json() 等任何空值比较都是 true

3. discarded 值永远不与自己相等。

discarded 是解析(sax/回调场景)内部使用的占位类型,宏为 value_t::discarded 返回 unordered_result,即 false。换句话说,json(json::value_t::discarded) == json(json::value_t::discarded) 结果是 false——这是有意的设计,使"被丢弃的占位值"永远不会参与相等匹配。测试 tests/src/unit-comparison.cpp 中构造了含 discarded 的笛卡尔积比较矩阵来验证这些行为。

浮点数的比较:精度与容差

默认行为:JSON 内的浮点数用 json::number_float_t::operator==(默认即 double::operator==)进行精确比较。这意味着 0.1 + 0.2 这类二进制浮点表示误差会导致预期外的 false

文档给出了两种工程化应对:

方案一:定义 epsilon 容差辅助函数

template<typename T, typename = typename std::enable_if<std::is_floating_point<T>::value, T>::type>
inline bool is_same(T a, T b, T epsilon = std::numeric_limits<T>::epsilon()) noexcept
{
    return std::abs(a - b) <= epsilon;
}

std::numeric_limits<T>::epsilon() 是机器精度,若你的业务误差更大,可显式传入更大阈值(如 1e-6)。

方案二:定义自定义 JSON 比较函数

bool my_equal(const_reference lhs, const_reference rhs)
{
    const auto lhs_type = lhs.type();
    const auto rhs_type = rhs.type();
    if (lhs_type == rhs_type)
    {
        switch(lhs_type)
        {
            // 自定义情形
            case value_t::number_float:
                return std::abs(lhs - rhs) <= std::numeric_limits<float>::epsilon();
            // 其余类型与原始 operator== 保持一致
            ...
        }
    }
    ...
}

要点:当 lhs_type == rhs_type 时只对 number_float 分支使用容差比较,其他类型继续沿用类型匹配的精确语义;类型不同时(例如整数 vs 浮点)仍应回退到官方运算符或自行决定转换策略。若需比较字符串/容器元素为浮点的情况,可递归调用 my_equal 保证语义贯穿。

不同 basic_json 特化的比较差异(json vs ordered_json)

官方文档用一个现实案例提示:比较"结构内容相同、键顺序不同"的 JSON 对象,结果取决于使用的是 nlohmann::json 还是 nlohmann::ordered_json。考虑如下两个对象:

{
   "version": 1,
   "type": "integer"
}

{
   "type": "integer",
   "version": 1
}
  • nlohmann::json(默认 object_tstd::map,键有序)而言,两个对象相等,结果为 true
  • nlohmann::ordered_jsonobject_tordered_map,保留插入顺序)而言,由于键序不同,结果为 false

完整可运行示例见 docs/mkdocs/docs/examples/operator__equal__specializations.cpp

#include <iostream>
#include <iomanip>
#include <nlohmann/json.hpp>

using json = nlohmann::json;

int main()
{
    nlohmann::json uj1 = {{"version", 1}, {"type", "integer"}};
    nlohmann::json uj2 = {{"type", "integer"}, {"version", 1}};

    nlohmann::ordered_json oj1 = {{"version", 1}, {"type", "integer"}};
    nlohmann::ordered_json oj2 = {{"type", "integer"}, {"version", 1}};

    std::cout << std::boolalpha << (uj1 == uj2) << '\n' << (oj1 == oj2) << std::endl;
}

输出(对应 docs/mkdocs/docs/examples/operator__equal__specializations.output):

true
false

结论:判断对象相等性时,务必明确你操作的是哪种特化。若你的业务不关心键顺序,应优先使用默认 json 以获得与键序无关的相等判断;若需要保留文档原始顺序且要求顺序敏感的比较,则需留意 ordered_json 的这一语义。

完整示例:比较多种 JSON 类型

下列示例覆盖数组、对象、数值(含整浮转换)与字符串的比较,源文件为 docs/mkdocs/docs/examples/operator__equal.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.000000000000001L;
    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';
}

输出(docs/mkdocs/docs/examples/operator__equal.output):

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

逐行解读:

  • 数组 [1,2,3][1,2,4] 长度相同但元素不同 → false
  • 对象 {"A":"a","B":"b"}{"B":"b","A":"a"} 键序不同但在默认 jsonstd::map 存储)下内容等价 → true
  • 整数 17 与浮点 17.000000000000001L 类型不同,但跨类型自动转换后数值相等 → true
  • 字符串 "foo""bar" 内容不同 → false

完整示例:与 nullptr(JSON null)比较

源文件 docs/mkdocs/docs/examples/operator__equal__nullptr_t.cpp 演示各类 JSON 值与 nullptr 的比较——这是标量重载(重载 2)中最常用的场景之一,等价于检查"这个值是否为 JSON null":

#include <iostream>
#include <nlohmann/json.hpp>

using json = nlohmann::json;

int main()
{
    // create several JSON values
    json array = {1, 2, 3};
    json object = {{"A", "a"}, {"B", "b"}};
    json number = 17;
    json string = "foo";
    json null;

    // output values and comparisons
    std::cout << std::boolalpha;
    std::cout << array << " == nullptr " << (array == nullptr) << '\n';
    std::cout << object << " == nullptr " << (object == nullptr) << '\n';
    std::cout << number << " == nullptr " << (number == nullptr) << '\n';
    std::cout << string << " == nullptr " << (string == nullptr) << '\n';
    std::cout << null << " == nullptr " << (null == nullptr) << '\n';
}

输出(docs/mkdocs/docs/examples/operator__equal__nullptr_t.output):

[1,2,3] == nullptr false
{"A":"a","B":"b"} == nullptr false
17 == nullptr false
"foo" == nullptr false
null == nullptr true

nullptr 被转换为 JSON null 后再参与比较,因此只有同为 null 的值才返回 true。注意它与 is_null() 成员函数判定的等价性:j == nullptr 本质等同于 j.is_null()

与相邻运算符的关系

  • operator!=:不相等比较。在 C++20 之前它由 !(lhs == rhs) 显式定义(include/nlohmann/json.hpp);在 C++20 之后可通过 == 自动重写得到。operator!= 的语义是 == 的严格逻辑取反。
  • operator<=>:C++20 三路比较。==<=> 使用同一套 JSON_IMPLEMENT_OPERATOR 宏逻辑(include/nlohmann/json.hpp),因此 NaN 无序、跨数值类型自动转换等规则在两者间保持一致。

版本历史

  1. 重载 1 于 1.0.0 加入;3.11.0 起在 C++20 下提供成员函数形式。
  2. 重载 2 于 1.0.0 加入;3.11.0 起在 C++20 下提供成员函数形式。

使用建议与注意事项汇总

  1. 默认精确比较operator== 对浮点执行精确比较,涉及浮点运算结果的相等性判断前,先考虑精度问题,必要时采用 epsilon 方案。
  2. NaN 永不相等:即使两侧是同一个 NaN,比较也返回 false;不要把 == 当作判断"是否存在 NaN"的手段。
  3. 区分 jsonordered_json:需要键序无关的语义用 json;需要顺序敏感语义用 ordered_json,并接受其"相同内容不同键序不相等"的结果。
  4. discarded 不参与任何相等匹配:这保证了解析回调等内部占位不会污染业务数据比较。
  5. 复杂度是线性的:对超大数组/对象做频繁相等性比较时,可先比较类型与 size() 做快速失败短路,再进入元素级比较。

以上结论均已由 include/nlohmann/json.hpp 中的宏实现与 compares_unordered 逻辑、tests/src/unit-comparison.cpp 的比较矩阵测试以及文档自带示例 docs/mkdocs/docs/examples 交叉验证,可作为工程决策的可靠依据。

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

项目优选

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