JSON for Modern C++:json_pointer::operator== 相等性比较的接口语义、异常行为与源码实现解析
在 C++ 中操作 JSON 文档时,nlohmann::json_pointer(JSON Pointer,RFC 6901 风格的引用路径)常被用来定位、比较和构造文档中的值。本文围绕 JSON for Modern C++ 的 API 文档 operator== 展开,完整覆盖两个 operator== 重载的签名、参数与返回值语义、异常安全保证和复杂度特性,并结合 json_pointer.hpp 中的实际实现,说明“按引用 token 序列比较”这一底层机制如何决定比较结果,以及字符串比较重载为何会触发 parse_error.107/108 异常、为何在 3.11.2 版本中被标记弃用。读完本文,你可以准确使用该运算符、正确捕获字符串转换过程中的异常,并理解 C++20 三向比较引入前后两套声明形态的差异。
接口声明:两个重载的完整签名
文档给出的声明分为 C++20 之前(自由函数模板)与 C++20 起(成员函数)两种形态:
// until C++20
template<typename RefStringTypeLhs, typename RefStringTypeRhs>
bool operator==(
const json_pointer<RefStringTypeLhs>& lhs,
const json_pointer<RefStringTypeRhs>& rhs) noexcept; // (1)
template<typename RefStringTypeLhs, typename StringType>
bool operator==(
const json_pointer<RefStringTypeLhs>& lhs,
const StringType& rhs); // (2)
template<typename RefStringTypeRhs, typename StringType>
bool operator==(
const StringType& lhs,
const json_pointer<RefStringTypeRhs>& rhs); // (2)
// since C++20
class json_pointer {
template<typename RefStringTypeRhs>
bool operator==(
const json_pointer<RefStringTypeRhs>& rhs) const noexcept; // (1)
bool operator==(const string_t& rhs) const; // (2)
};
两个重载的语义(文档 “Parameters/Return value” 部分):
- 重载 (1):比较两个 JSON Pointer 是否相等,比较依据是它们的引用 token(reference token)序列;
- 重载 (2):将一个 JSON Pointer 与字符串(或字符串与 JSON Pointer)比较,先把字符串转换成 JSON Pointer,再按重载 (1) 的规则比较。
模板参数说明:RefStringTypeLhs、RefStringTypeRhs 分别是左/右操作数 JSON Pointer 的字符串类型;StringType 是由 json_pointer 操作数推导出的字符串类型,即 json_pointer::string_t。参数 lhs、rhs 分别为要比较的第一个和第二个值,返回值即二者是否相等。
在源码层面,json_pointer.hpp 用宏 JSON_HAS_THREE_WAY_COMPARISON 切换了两套声明:C++20 下是类内成员函数(json_pointer.hpp#L976-L998),非 C++20 下是类内声明、类外定义的自由函数(json_pointer.hpp#L1002-L1019,定义位于 json_pointer.hpp#L1054-L1079)。文档中注明 C++20 成员函数自 3.11.2 版本加入,与源码中的版本注释一致。
比较的核心语义:按引用 token 序列逐一对比
operator== 的相等性不取决于原始字符串是否逐字符相同,而取决于解析后的引用 token 序列是否一致。从源码结构看,这一点非常直接:
// include/nlohmann/detail/json_pointer.hpp
template<typename RefStringTypeLhs, typename RefStringTypeRhs>
inline bool operator==(const json_pointer<RefStringTypeLhs>& lhs,
const json_pointer<RefStringTypeRhs>& rhs) noexcept
{
return lhs.reference_tokens == rhs.reference_tokens;
}
reference_tokens 是 json_pointer 的私有成员 std::vector<string_t>(json_pointer.hpp#L1049-L1052)。构造函数把字符串交给 split() 分解为 token 序列(json_pointer.hpp#L60-L64),因此 ""、"/foo"、"/foo/0" 这类指针内部保存的都是已还原转义的 token 列表。比较时等价于对两个 std::vector<string_t> 做逐元素相等判断:
- 两侧 token 数量不同,比较在长度判断处即结束(常量时间);
- 数量相同时逐个 token 比较(线性时间)。
这正是文档 “Complexity” 一节所述:“若 lhs 和 rhs 的引用 token 数量不同则为常量复杂度,否则与引用 token 数量成线性复杂度。”
字符串比较重载:先转换再比较,并可能抛异常
重载 (2) 的实现同样简短——先把字符串隐式构造为同类型的 json_pointer,再走重载 (1):
template<typename RefStringTypeLhs,
typename StringType = typename json_pointer<RefStringTypeLhs>::string_t>
JSON_HEDLEY_DEPRECATED_FOR(3.11.2, operator==(json_pointer, json_pointer))
inline bool operator==(const json_pointer<RefStringTypeLhs>& lhs,
const StringType& rhs)
{
return lhs == json_pointer<RefStringTypeLhs>(rhs);
}
关键在于字符串到 json_pointer 的转换发生在 split() 中,它会对非法指针字符串抛出异常(json_pointer.hpp#L789-L840),因此重载 (1) 是无异常保证的,而重载 (2) 可能抛出 parse_error:
| 异常 | 触发条件 | 源码位置 |
|---|---|---|
| parse_error.107 | 给定的 JSON Pointer 字符串非空且未以 / 开头 |
json_pointer.hpp#L805 |
| parse_error.108 | 字符串中的 ~ 后面不是 0(代表 ~)或 1(代表 /) |
json_pointer.hpp#L840 |
这解释了文档 “Exception safety” 一节的两条差异:重载 (1) 是 No-throw guarantee(永不抛异常);重载 (2) 是 strong exception safety——若发生异常,原值保持完好(因为比较不修改任何操作数)。
可运行示例一:比较两个 JSON Pointer(重载 1)
以下示例来自 json_pointer__operator__equal.cpp,可直接编译运行(头文件模式包含 single_include/nlohmann/json.hpp 即可):
#include <iostream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
// different JSON pointers
json::json_pointer ptr0; // 空指针,指向整个文档
json::json_pointer ptr1(""); // 显式传入空串,与 ptr0 等价
json::json_pointer ptr2("/foo");
// compare JSON pointers
std::cout << std::boolalpha
<< "\"" << ptr0 << "\" == \"" << ptr0 << "\": " << (ptr0 == ptr0) << '\n'
<< "\"" << ptr0 << "\" == \"" << ptr1 << "\": " << (ptr0 == ptr1) << '\n'
<< "\"" << ptr1 << "\" == \"" << ptr2 << "\": " << (ptr1 == ptr2) << '\n'
<< "\"" << ptr2 << "\" == \"" << ptr2 << "\": " << (ptr2 == ptr2) << std::endl;
}
输出(见 json_pointer__operator__equal.output):
"" == "": true
"" == "": true
"" == "/foo": false
"/foo" == "/foo": true
注意 ptr0 与 ptr1 的来源不同(默认构造 vs. 空字符串构造),但二者 token 序列都为空,因此比较结果为 true——这印证了“比较的是 token 序列而非构造方式”的语义。
可运行示例二:比较 JSON Pointer 与字符串(重载 2,注意异常)
第二个示例来自 json_pointer__operator__equal_stringtype.cpp,展示了字符串比较以及非法字符串触发异常的情形:
#include <exception>
#include <iostream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
// different JSON pointers
json::json_pointer ptr0;
json::json_pointer ptr1("");
json::json_pointer ptr2("/foo");
// different strings
std::string str0("");
std::string str1("/foo");
std::string str2("bar");
// compare JSON pointers and strings
std::cout << std::boolalpha
<< "\"" << ptr0 << "\" == \"" << str0 << "\": " << (ptr0 == str0) << '\n'
<< "\"" << str0 << "\" == \"" << ptr1 << "\": " << (str0 == ptr1) << '\n'
<< "\"" << ptr2 << "\" == \"" << str1 << "\": " << (ptr2 == str1) << std::endl;
try
{
std::cout << "\"" << str2 << "\" == \"" << ptr2 << "\": " << (str2 == ptr2) << std::endl;
}
catch (const json::parse_error& ex)
{
std::cout << ex.what() << std::endl;
}
}
输出(见 json_pointer__operator__equal_stringtype.output):
"" == "": true
"" == "": true
"/foo" == "/foo": true
"bar" == "/foo": [json.exception.parse_error.107] parse error at byte 1: JSON pointer must be empty or begin with '/' - was: 'bar'
最后一行正是 parse_error.107:字符串 "bar" 非空且不以 / 开头,split() 在转换阶段即抛出异常,比较从未发生。异常信息中还带出了出错字节位置(byte 1),与库的 异常诊断 机制一致。
弃用说明与 C++20 演进
文档在 “Notes” 中给出明确警告:重载 (2)(字符串比较)已被弃用,并将在未来的大版本中移除。源码中对应变了双重体现:
- 所有字符串比较重载都标注了
JSON_HEDLEY_DEPRECATED_FOR(3.11.2, operator==(json_pointer, json_pointer))(json_pointer.hpp#L987-L991、json_pointer.hpp#L1063-L1079),编译时若启用弃用警告会提示迁移到“先显式构造json_pointer再比较两个指针”的写法; - C++20 下(
JSON_HAS_THREE_WAY_COMPARISON成立),库额外提供了成员形式operator==以及成员operator<=>(json_pointer.hpp#L993-L998),!=等比较可由三向比较推导,与文档 “Version history” 中“C++20 成员函数于 3.11.2 加入”的记录对应。
迁移建议(由弃用注解可推断):将 ptr == "/foo" 改写为 ptr == json::json_pointer("/foo"),既消除弃用警告,又让转换时机与异常行为显式化。
版本历史与相关接口
文档 “Version history” 一节:
- 重载 (1):2.1.0 版本加入;C++20 成员函数于 3.11.2 加入;
- 重载 (2):为向后兼容而加入,3.11.2 版本起弃用。
围绕 JSON Pointer 的其余比较与操纵接口,可参考同一 API 目录下的文档:operator!=、string_t、json_pointer 类总览,实现均集中在 json_pointer.hpp 中。本文基于当前仓库(版本 3.12.0,见 json_pointer.hpp 文件头注释)核实,所有行号与宏名均可在源码中直接定位验证。
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
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python07
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00