首页
/ JSON for Modern C++:`operator!=` 不等比较详解与特殊值语义

JSON for Modern C++:`operator!=` 不等比较详解与特殊值语义

2026-09-07 16:19:24作者:庞眉杨Will

本篇文章聚焦 nlohmann/json(JSON for Modern C++)中 basic_json 的不等比较运算符 operator!=,完整讲解其函数签名(含 C++20 前后的形态变化)、比较语义、特殊值(NaNdiscarded)行为、版本演进以及可运行的代码示例。读完你将能够在日常代码中准确判断两个 JSON 值是否不等,理解为何 NaN != NaNdiscarded != x 会返回 true,并清楚底层实现与 operator==operator<=> 的关系。

定位:六类比较运算符中的不等比较

basic_json 为 JSON 值提供了完整的比较运算符族:==!=<<=>>=,以及 C++20 的三路比较 <=>operator!= 用于判断两个 JSON 值“不相等”,其语义并非独立实现一套复杂的规则,而是被精确定义为 operator== 的逻辑取反,即 #!cpp !(lhs == rhs)

这一点在官方 API 文档 operator_ne.md 中有着明确且一致的表述,也是理解本运算符全部行为的钥匙——只要弄清楚 == 在何种情况下成立,!= 的所有结果都可以直接推导出来。

函数签名:C++20 前后两种形态

operator!= 存在两组重载:

  1. 两个 JSON 值比较(重载 1)
  2. 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_referencebasic_json::const_reference(对 type 为 JSON 对象的容器类型的常引用)。语义说明如下:

  • 重载 1:比较两个 JSON 值是否不等。C++20 之前为 #!cpp !(lhs == rhs),C++20 起为 #!cpp !(*this == rhs)。也就是说,比较结果就是 operator== 的逻辑否定,特殊值(如 NaNdiscarded)也不例外。
  • 重载 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-L3812operator==operator<=> 均为成员函数;按照 C++20 的规则,operator== 一旦可用,编译器会自动为 != 生成取反的 rewritten candidate,因此成员形态的 operator!= 无需重复实现,文档中的声明用于说明这一 C++20 下的等价调用形式。

模板参数与参数说明

operator!= 相关的模板参数、函数参数与原文档保持一致,如下:

  • 模板参数 ScalarType:满足 std::is_scalar<ScalarType>::value 的标量类型(如 intdoubleboolnullptr_t、枚举等)。
  • 参数 lhs(in):待比较的第一个值(成员函数形式下为 *this)。
  • 参数 rhs(in):待比较的第二个值。

两个重载均返回 bool,即 lhs/*thisrhs 是否不相等。

底层比较如何完成:JSON_IMPLEMENT_OPERATOR

既然 operator!= 是对 operator== 的取反,那 == 又是如何比较的?这要追溯到 operator== 内部使用的宏 JSON_IMPLEMENT_OPERATORinclude/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::vectorstd::map 的元素比较、double 的直接等值比较等),复杂度与元素个数线性相关。
  • 类型不同但同为数值(例如一端为 number_integer、另一端为 number_float)时,会进入后续的 else if 分支做整数/浮点的自动转换再比较,这正是示例中 json(17) == json(17.0) 返回 true 的原因。
  • nullnullnull_resulttrue,即两个 JSON null 相等,因此 null != nullfalse
  • discarded 类型:落入 unordered_result = false,即任何 discarded == x 都为 false,从而 discarded != x 恒为 true
  • 对非数值的异类型比较(如字符串 vs 数组),会走宏尾部比较 value_t 枚举的默认分支,类型不同即视为不相等。

正因 == 采用底层原生 double::operator== 做浮点比较,库文档(见 operator_eq.md 中“Comparing floating-point numbers”一节)特别提醒:若需要基于 epsilon 的容差比较浮点,应自行编写比较函数,而不是直接依赖 ==/!=

特殊值行为:NaNdiscarded

原文档中专门以 note 形式强调了特殊值在 != 下的行为。由于 operator!= 被定义为 !(a == b),其行为完全跟随 operator==

  • NaN:浮点 NaN 在数值域内是“无序”的——NaN == NaNfalse,因此 NaN != NaNtrue。同理 NaN 与任何其他数值比较时,NaN != x 也为 true
  • discardeddiscarded 值不会与任何值(包括它自己)相等,discarded == x 对任意 x 均为 false,因此 discarded != x 恒为 true

需要说明的是,这一行为并非历来如此。原文档“Version history”明确记录:

Changed in version 3.13.0 to remove special-casing for NaN and discarded values; operator!= now consistently means !(a == b).

即在 3.13.0 之前operator!= 曾对 NaNdiscarded 做了特殊处理(先判断是否处于“不可比较/无序”状态,若是则直接返回 false,表现为 NaN != NaNdiscarded != 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. 数组逐元素比较:[1,2,3][1,2,4] 仅在尾元素不同,即判定不相等;
  2. JSON 对象与成员顺序无关{"A":"a","B":"b"}{"B":"b","A":"a"} 内容相同,!=false。默认 nlohmann::json 基于 std::map(有序键),但比较的是键值对集合而非插入顺序,因此顺序不影响相等性;
  3. 整数与浮点自动转换:1717.0 数值相等,!=false
  4. 字符串按内容比较:"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 nullnullptr 相等,!= 返回 false

operator==operator<=> 的关系及工程建议

basic_json 的比较体系中:

  • operator!= = !(operator==),两者永远互补,不存在第三个结果;
  • C++20 下三路比较 operator<=> 返回 std::partial_ordering,可同时推导出 <<=>>=,而 ==/!= 仍按相等语义独立判定(discardedNaN 在三路比较中被视为 unordered)。

因此推荐的最佳实践是:

  • 判断“是否不等”优先使用 !=(或 == 后取反),可读性最好;
  • 需要容差比较浮点时,不要依赖 ==/!= 的默认浮点等值语义(底层是 double::operator==),应自行实现带 epsilon 的比较函数(思路可参考 operator_eq.md 的 notes 部分);
  • 不同 basic_json 特化(如 nlohmann::json 与保持插入序的 nlohmann::ordered_json)在对象顺序相关场景下的比较可能给出不同结论,跨特化比较前需先确认类型,详见 ordered_json.mdoperator_eq.md

版本历史

据官方 API 文档 operator_ne.md 记录:

  • 两个重载均添加于 1.0.0
  • 3.11.0 起提供 C++20 成员函数形式;
  • 3.13.0 起移除对 NaNdiscarded 的特殊处理,operator!= 统一为 !(a == b)

在单元测试中,这一系列行为由 tests/src/unit-comparison.cpp 集中覆盖(其中包含大量 != 相关断言,包括多类型组合、nullptr 比较与特殊值场景),可作为进一步验证与深挖实现细节的入口。结合源码实现(include/nlohmann/json.hpp#L3893-L3916)阅读,可以确认:operator!= 本质上是一层极薄的语义层,所有复杂规则都收敛于 operator==JSON_IMPLEMENT_OPERATOR 宏——理解这一点,即可对任意两个 JSON 值的“不等”判断结果做出准确预测。

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

项目优选

收起
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