JSON for Modern C++:`operator!=` 不等比较详解与特殊值语义
本篇文章聚焦 nlohmann/json(JSON for Modern C++)中 basic_json 的不等比较运算符 operator!=,完整讲解其函数签名(含 C++20 前后的形态变化)、比较语义、特殊值(NaN 与 discarded)行为、版本演进以及可运行的代码示例。读完你将能够在日常代码中准确判断两个 JSON 值是否不等,理解为何 NaN != NaN 与 discarded != x 会返回 true,并清楚底层实现与 operator==、operator<=> 的关系。
定位:六类比较运算符中的不等比较
basic_json 为 JSON 值提供了完整的比较运算符族:==、!=、<、<=、>、>=,以及 C++20 的三路比较 <=>。operator!= 用于判断两个 JSON 值“不相等”,其语义并非独立实现一套复杂的规则,而是被精确定义为 operator== 的逻辑取反,即 #!cpp !(lhs == rhs)。
这一点在官方 API 文档 operator_ne.md 中有着明确且一致的表述,也是理解本运算符全部行为的钥匙——只要弄清楚 == 在何种情况下成立,!= 的所有结果都可以直接推导出来。
函数签名:C++20 前后两种形态
operator!= 存在两组重载:
- 两个 JSON 值比较(重载 1)
- JSON 值与标量(scalar)互比较(重载 2,模板版本)
在 C++20 之前,它们以 friend 自由函数的形式提供;自 C++20 起,改为 basic_json 的成员函数(同时库会启用三路比较 <=>,operator== 也被改写为成员函数形态):
// 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)
};
其中 const_reference 即 basic_json::const_reference(对 type 为 JSON 对象的容器类型的常引用)。语义说明如下:
- 重载 1:比较两个 JSON 值是否不等。C++20 之前为
#!cpp !(lhs == rhs),C++20 起为#!cpp !(*this == rhs)。也就是说,比较结果就是operator==的逻辑否定,特殊值(如NaN、discarded)也不例外。 - 重载 2:把标量转换为 JSON 值后,再按重载 1 的规则比较两个 JSON 值,从而支持“JSON 值 != 标量”与“标量 != JSON 值”两种写法。
源码中的实际形态
在头文件 include/nlohmann/json.hpp 中,C++20 之前的 friend 版本实现非常直白(include/nlohmann/json.hpp#L3893-L3916):
friend bool operator!=(const_reference lhs, const_reference rhs) noexcept
{
return !(lhs == rhs);
}
template<typename ScalarType, typename std::enable_if<
std::is_scalar<ScalarType>::value, int>::type = 0>
friend bool operator!=(const_reference lhs, ScalarType rhs) noexcept
{
return lhs != basic_json(rhs);
}
template<typename ScalarType, typename std::enable_if<
std::is_scalar<ScalarType>::value, int>::type = 0>
friend bool operator!=(ScalarType lhs, const_reference rhs) noexcept
{
return basic_json(lhs) != rhs;
}
可以确认:库并未为 != 单独编写元素级比较逻辑,而是完整复用了 operator== 的实现。
而在 C++20 分支(#if JSON_HAS_THREE_WAY_COMPARISON)内,从源码结构看 include/nlohmann/json.hpp#L3767-L3812,operator== 与 operator<=> 均为成员函数;按照 C++20 的规则,operator== 一旦可用,编译器会自动为 != 生成取反的 rewritten candidate,因此成员形态的 operator!= 无需重复实现,文档中的声明用于说明这一 C++20 下的等价调用形式。
模板参数与参数说明
operator!= 相关的模板参数、函数参数与原文档保持一致,如下:
- 模板参数
ScalarType:满足std::is_scalar<ScalarType>::value的标量类型(如int、double、bool、nullptr_t、枚举等)。 - 参数
lhs(in):待比较的第一个值(成员函数形式下为*this)。 - 参数
rhs(in):待比较的第二个值。
两个重载均返回 bool,即 lhs/*this 与 rhs 是否不相等。
底层比较如何完成:JSON_IMPLEMENT_OPERATOR 宏
既然 operator!= 是对 operator== 的取反,那 == 又是如何比较的?这要追溯到 operator== 内部使用的宏 JSON_IMPLEMENT_OPERATOR(include/nlohmann/json.hpp#L3664-L3704):
#define JSON_IMPLEMENT_OPERATOR(op, null_result, unordered_result, default_result) \
const auto lhs_type = lhs.type(); \
const auto rhs_type = rhs.type(); \
\
if (lhs_type == rhs_type) /* NOLINT(readability/braces) */ \
{ \
switch (lhs_type) \
{ \
case value_t::array: \
return (*lhs.m_data.m_value.array) op (*rhs.m_data.m_value.array); \
case value_t::object: \
return (*lhs.m_data.m_value.object) op (*rhs.m_data.m_value.object); \
case value_t::null: \
return (null_result); \
case value_t::string: \
return (*lhs.m_data.m_value.string) op (*rhs.m_data.m_value.string); \
case value_t::boolean: \
return (lhs.m_data.m_value.boolean) op (rhs.m_data.m_value.boolean); \
case value_t::number_integer: \
return (lhs.m_data.m_value.number_integer) op (rhs.m_data.m_value.number_integer); \
case value_t::number_unsigned: \
return (lhs.m_data.m_value.number_unsigned) op (rhs.m_data.m_value.number_unsigned); \
case value_t::number_float: \
return (lhs.m_data.m_value.number_float) op (rhs.m_data.m_value.number_float); \
case value_t::binary: \
return (*lhs.m_data.m_value.binary) op (*rhs.m_data.m_value.binary); \
case value_t::discarded: \
default: \
return (unordered_result); \
} \
} \
else if (lhs_type == value_t::number_integer && rhs_type == value_t::number_float) \
...
从宏的参数 JSON_IMPLEMENT_OPERATOR( ==, true, false, false) 可以读出 operator== 的关键结论:
- 若两侧 JSON 类型相同,则委托给对应底层容器/标量的原生
operator==(std::vector、std::map的元素比较、double的直接等值比较等),复杂度与元素个数线性相关。 - 类型不同但同为数值(例如一端为
number_integer、另一端为number_float)时,会进入后续的else if分支做整数/浮点的自动转换再比较,这正是示例中json(17) == json(17.0)返回true的原因。 null与null:null_result为true,即两个 JSONnull相等,因此null != null为false。discarded类型:落入unordered_result = false,即任何discarded == x都为false,从而discarded != x恒为true。- 对非数值的异类型比较(如字符串 vs 数组),会走宏尾部比较
value_t枚举的默认分支,类型不同即视为不相等。
正因 == 采用底层原生 double::operator== 做浮点比较,库文档(见 operator_eq.md 中“Comparing floating-point numbers”一节)特别提醒:若需要基于 epsilon 的容差比较浮点,应自行编写比较函数,而不是直接依赖 ==/!=。
特殊值行为:NaN 与 discarded
原文档中专门以 note 形式强调了特殊值在 != 下的行为。由于 operator!= 被定义为 !(a == b),其行为完全跟随 operator==:
NaN:浮点NaN在数值域内是“无序”的——NaN == NaN为false,因此NaN != NaN为true。同理NaN与任何其他数值比较时,NaN != x也为true。discarded:discarded值不会与任何值(包括它自己)相等,discarded == x对任意x均为false,因此discarded != x恒为true。
需要说明的是,这一行为并非历来如此。原文档“Version history”明确记录:
Changed in version 3.13.0 to remove special-casing for
NaNanddiscardedvalues;operator!=now consistently means!(a == b).
即在 3.13.0 之前,operator!= 曾对 NaN 与 discarded 做了特殊处理(先判断是否处于“不可比较/无序”状态,若是则直接返回 false,表现为 NaN != NaN 与 discarded != x 均为 false);自 3.13.0 起去除了这些特例,!= 被统一为对 == 的严格逻辑取反,语义更加一致。从源码遗留痕迹来看,include/nlohmann/json.hpp#L3814-L3828 附近仍保留着 JSON_USE_LEGACY_DISCARDED_VALUE_COMPARISON 宏所包裹的旧式 discarded 比较兼容代码,并在 3.11.0 起以 JSON_HEDLEY_DEPRECATED_FOR 标注建议用户移除以回到标准语义,进一步印证了这一演进过程。
因此在实际工程中请务必注意:在 3.13.0 及以上版本,若 JSON 值中存在 NaN(例如反序列化 [1, NaN])或 discarded(例如使用 parser callback 丢弃的节点),!= 的结果将总是 true,这与对一般可比较数值的直觉不同。
复杂度与异常安全
- 异常安全:
No-throw。两个重载都声明为noexcept,在比较过程中不会抛出异常。 - 复杂度:线性。因为
operator==需要逐元素比较数组/对象成员或进行标量比较,!=作为其取反同样为线性复杂度(对单个标量则为常数时间)。
完整可运行示例
原文档提供了两个可直接编译运行的示例,完整继承如下(仓库路径见 docs/mkdocs/docs/examples/)。
示例一:比较多种 JSON 类型
源码位于 examples/operator__notequal.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';
}
输出(对应 examples/operator__notequal.output):
[1,2,3] != [1,2,4] true
{"A":"a","B":"b"} != {"A":"a","B":"b"} false
17 != 17.0 false
"foo" != "bar" true
示例透露出四个重要比较规则:
- 数组逐元素比较:
[1,2,3]与[1,2,4]仅在尾元素不同,即判定不相等; - JSON 对象与成员顺序无关:
{"A":"a","B":"b"}与{"B":"b","A":"a"}内容相同,!=为false。默认nlohmann::json基于std::map(有序键),但比较的是键值对集合而非插入顺序,因此顺序不影响相等性; - 整数与浮点自动转换:
17与17.0数值相等,!=为false; - 字符串按内容比较:
"foo"与"bar"不等。
示例二:与 nullptr(JSON null)比较
源码位于 examples/operator__notequal__nullptr_t.cpp:
#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';
}
输出(对应 examples/operator__notequal__nullptr_t.output):
[1,2,3] != nullptr true
{"A":"a","B":"b"} != nullptr true
17 != nullptr true
"foo" != nullptr true
null != nullptr false
该示例演示了重载 2(标量比较):nullptr 会被转换为 JSON null 值后再参与比较。因此数组、对象、数值、字符串与 JSON null 均不等(返回 true),而 JSON null 与 nullptr 相等,!= 返回 false。
与 operator==、operator<=> 的关系及工程建议
在 basic_json 的比较体系中:
operator!==!(operator==),两者永远互补,不存在第三个结果;- C++20 下三路比较
operator<=>返回std::partial_ordering,可同时推导出<、<=、>、>=,而==/!=仍按相等语义独立判定(discarded与NaN在三路比较中被视为unordered)。
因此推荐的最佳实践是:
- 判断“是否不等”优先使用
!=(或==后取反),可读性最好; - 需要容差比较浮点时,不要依赖
==/!=的默认浮点等值语义(底层是double::operator==),应自行实现带epsilon的比较函数(思路可参考 operator_eq.md 的 notes 部分); - 不同
basic_json特化(如nlohmann::json与保持插入序的nlohmann::ordered_json)在对象顺序相关场景下的比较可能给出不同结论,跨特化比较前需先确认类型,详见 ordered_json.md 与 operator_eq.md。
版本历史
据官方 API 文档 operator_ne.md 记录:
- 两个重载均添加于 1.0.0;
- 3.11.0 起提供 C++20 成员函数形式;
- 3.13.0 起移除对
NaN与discarded的特殊处理,operator!=统一为!(a == b)。
在单元测试中,这一系列行为由 tests/src/unit-comparison.cpp 集中覆盖(其中包含大量 != 相关断言,包括多类型组合、nullptr 比较与特殊值场景),可作为进一步验证与深挖实现细节的入口。结合源码实现(include/nlohmann/json.hpp#L3893-L3916)阅读,可以确认:operator!= 本质上是一层极薄的语义层,所有复杂规则都收敛于 operator== 的 JSON_IMPLEMENT_OPERATOR 宏——理解这一点,即可对任意两个 JSON 值的“不等”判断结果做出准确预测。
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 StartedRust0630
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