JSON for Modern C++ 之 basic_json::operator==:相等性比较的完整语义与源码解析
本篇技术指南以 JSON for Modern C++(nlohmann/json)的官方 API 文档 docs/mkdocs/docs/api/basic_json/operator_eq.md 为骨架,系统讲解 basic_json 相等性比较运算符 == 的完整语义:从函数签名、比较规则、NaN/null/discarded 等特殊值行为,到浮点数精度陷阱与不同 basic_json 特化(json 与 ordered_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)
};
两个重载的用途:
- JSON 与 JSON 的比较:比较两个 JSON 值是否相等。
- 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_t(bool)的比较 |
number_integer / number_unsigned / number_float |
对应数值类型的比较(浮点默认是 double,即 json::number_float_t) |
binary |
底层 binary_t(byte_container_with_subtype)的比较 |
null |
恒定返回比较为真(同一类型 null 彼此相等) |
discarded / 默认 |
落入"无序"分支,比较结果不成立 |
跨数值类型的自动转换
当两侧是不同数值类型时,宏在进入 switch 之前会先做转换(include/nlohmann/json.hpp):
number_integervsnumber_float、number_unsignedvsnumber_float:整数值被static_cast<number_float_t>提升为浮点后比较;number_unsignedvsnumber_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/*this与rhs是否相等。 - 异常安全:无异常保证(no-throw guarantee),
noexcept声明,此函数从不抛出异常。即便值内部是数组或对象,其比较所依赖的容器比较操作也要求不抛异常(GCC 下甚至显式忽略-Wfloat-equal警告,见 include/nlohmann/json.hpp)。 - 复杂度:线性(Linear)。最坏情况下需要比较容器中每个元素,实际取决于两侧 JSON 的规模与形态。
特殊值的比较语义(重点注意事项)
文档对三类容易踩坑的特殊值给出了明确约定,这在 include/nlohmann/json.hpp 的 compares_unordered 实现中有对应逻辑支撑:
1. NaN 在数值域内是"无序"的,以下比较全部返回 false:
- 一个
NaN与它自身比较; - 一个
NaN与另一个NaN比较; - 一个
NaN与任何其他数字比较。
这是 IEEE 754 浮点语义的直接体现。compares_unordered 会检测"某一方是浮点 NaN 且另一方是任意数值类型"的组合并提前判定为不相等。
2. JSON null 值彼此全部相等。
因为宏对 value_t::null 直接返回 true(null_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_t为std::map,键有序)而言,两个对象相等,结果为true; - 对
nlohmann::ordered_json(object_t为ordered_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"}键序不同但在默认json(std::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.0.0 加入;3.11.0 起在 C++20 下提供成员函数形式。
- 重载 2 于 1.0.0 加入;3.11.0 起在 C++20 下提供成员函数形式。
使用建议与注意事项汇总
- 默认精确比较:
operator==对浮点执行精确比较,涉及浮点运算结果的相等性判断前,先考虑精度问题,必要时采用 epsilon 方案。 NaN永不相等:即使两侧是同一个NaN,比较也返回false;不要把==当作判断"是否存在 NaN"的手段。- 区分
json与ordered_json:需要键序无关的语义用json;需要顺序敏感语义用ordered_json,并接受其"相同内容不同键序不相等"的结果。 discarded不参与任何相等匹配:这保证了解析回调等内部占位不会污染业务数据比较。- 复杂度是线性的:对超大数组/对象做频繁相等性比较时,可先比较类型与
size()做快速失败短路,再进入元素级比较。
以上结论均已由 include/nlohmann/json.hpp 中的宏实现与 compares_unordered 逻辑、tests/src/unit-comparison.cpp 的比较矩阵测试以及文档自带示例 docs/mkdocs/docs/examples 交叉验证,可作为工程决策的可靠依据。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0629
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00