nlohmann/json:用 json_pointer::push_back 动态构建 JSON Pointer 路径
本文围绕 JSON for Modern C++(nlohmann/json)的 API 文档 json_pointer::push_back 展开,讲清这个重载函数在库中的准确签名与语义:向一个已存在的 JSON Pointer 末尾追加一个未转义的引用 token。读懂本文后,你能掌握在运行时逐段拼装 JSON Pointer(例如先取根节点、再按字段名和数组下标逐级下钻)的方法,理解它与 operator/=、to_string 转义规则之间的协作关系,并知道其实现位于哪个源码文件、有哪些测试用例为其兜底。
接口签名与语义
官方文档给出的接口为两个重载:
void push_back(const string_t& token);
void push_back(string_t&& token);
- 第一个重载接收 token 的常量引用,适用于 token 之后还要继续使用的场景;
- 第二个重载接收右值引用,内部通过
std::move转移字符串,避免一次不必要的拷贝,适合拼接临时构造的 token。
文档对行为的定义只有一句话,但信息量很大:Append an unescaped token at the end of the reference pointer——追加的是一个“未转义”的 token。这意味着调用方传入的是语义层面的原始字段名(例如 /、~、foo),JSON Pointer 规范的 ~0/~1 转义会在最终输出字符串时由库自动完成,调用者不需要也不应该手动转义。
参数与复杂度
| 项目 | 说明 |
|---|---|
token(入参) |
要追加到指针末尾的引用 token,类型为 string_t |
| 返回值 | 无(void),原地修改指针对象 |
| 时间复杂度 | 均摊常数(Amortized constant),即 std::vector::push_back 的复杂度特征 |
| 异常 | 文档未声明抛出条件;见下文源码分析 |
完整示例:从空指针逐级拼装出 /foo/0/bar
文档自带的示例演示了从空 JSON Pointer 出发、连续三次 push_back 的结果。示例源码见 json_pointer__push_back.cpp,期望输出见 json_pointer__push_back.output:
#include <iostream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
// create empty JSON Pointer
json::json_pointer ptr;
std::cout << "\"" << ptr << "\"\n";
// call push_back()
ptr.push_back("foo");
std::cout << "\"" << ptr << "\"\n";
ptr.push_back("0");
std::cout << "\"" << ptr << "\"\n";
ptr.push_back("bar");
std::cout << "\"" << ptr << "\"\n";
}
实际输出为:
""
"/foo"
"/foo/0"
"/foo/0/bar"
三段式演进展示了该接口的核心用法:
- 默认构造得到指向文档根的空指针,输出为
""; push_back("foo")追加对象键,得到/foo;push_back("0")追加数组下标(token 本身只是字符串,库不会区分“键”和“下标”的语义,区分发生在用指针访问 JSON 值时);push_back("bar")继续下钻,得到/foo/0/bar。
源码实现:一条对 vector 的透传
push_back 的实现在 json_pointer.hpp 中,两个重载的函数体都只有一行:
/// @brief append an unescaped token at the end of the reference pointer
void push_back(const string_t& token)
{
reference_tokens.push_back(token);
}
void push_back(string_t&& token)
{
reference_tokens.push_back(std::move(token));
}
从源码结构看,整个 json_pointer 类就是围绕一个私有成员组织的(json_pointer.hpp#L1049-L1052):
private:
/// the reference tokens
std::vector<string_t> reference_tokens;
push_back是std::vector::push_back的薄封装,因此“均摊常数”的复杂度承诺直接继承自底层容器的行为;- 右值重载使用
std::move,与文档中两个重载的区分一一对应; - 由于实现只是向量追加,没有额外校验逻辑,文档也未声明异常条件——空指针上
push_back是合法操作,正是上面示例的第一步。
与 operator/= 的关系:push_back 是组合算子的底座
同一个头文件里,operator/=(string_t token) 的重载(json_pointer.hpp#L106-L112)内部直接委托给 push_back:
/// @brief append an unescaped reference token at the end of this JSON pointer
json_pointer& operator/=(string_t token)
{
push_back(std::move(token));
return *this;
}
也就是说,ptr /= "foo" 与 ptr.push_back("foo") 效果等价(前者额外返回 *this 支持链式调用),此外 operator/= 还提供 std::size_t 数组下标与整个 json_pointer 的追加版本。写风格化代码可以用 /=,写通用逻辑时 push_back 是更直白的选择。
“未转义”的落点:转义发生在 to_string
“unescaped token”这一语义的另一半体现在输出端。to_string()(json_pointer.hpp#L66-L76)在把 token 序列还原成指针字符串时,对每个 token 调用 detail::escape 再拼上 / 分隔符。反过来,构造时从字符串解析的 split() 会对 ~0/~1 做 detail::unescape(json_pointer.hpp#L840-L847)。因此内部 reference_tokens 中保存的永远是原始 token,push_back 传入什么就存什么。
单元测试对这一闭环有明确验证,unit-json_pointer.cpp 中:
// push key which has to be encoded
ptr.push_back("object");
ptr.push_back("/");
CHECK(j[ptr] == j["object"]["/"]);
CHECK(ptr.to_string() == "/object/~1");
向指针里 push_back 一个字段名 / 后,to_string() 输出 /object/~1——转义由库完成;而用它索引 JSON 时又能正确命中 j["object"]["/"]。同一测试文件(unit-json_pointer.cpp#L546-L593)还覆盖了 push_back 与 pop_back、pop_front、parent_pointer() 的成对使用,例如逐层 push 出 answer/everything 后用 j[ptr] 与 j["answer"]["everything"] 断言等价。
典型应用场景:运行时逐段下钻
push_back 的主要价值在于“指针内容在编译期未知”的场景。静态情况下直接写字面量更省事:
json j = {{"foo", {{42, 43}, {"bar"}}}};
// 静态已知路径,直接用字面量
auto v = j["/foo/0/bar"_json_pointer];
而路径需要运行时拼装时,可以这样写:
json::json_pointer ptr; // 指向根
ptr.push_back("foo"); // "/foo"
for (std::size_t i = 0; i < 10; ++i) {
ptr.push_back(std::to_string(i)); // 移动语义,右值重载
}
ptr.push_back("bar");
与同类 API 搭配时,还能利用 pop_back 逐级回退:从 json_pointer.hpp#L195-L205 可以看到,pop_back 在空指针上会抛出 out_of_range(错误码 405,"JSON pointer has no parent"),因此“push 下去、pop 回来”的遍历模式需要自行保证不成对调用;判断指针是否为空应使用 empty()(json_pointer.hpp#L233-L236)。
另外注意与 basic_json::push_back 的区分:后者(向 JSON 值本身添加数组元素/对象键值对,见 single_include/nlohmann/json.hpp#L24511-L24513)修改的是 JSON 值,本文的 json_pointer::push_back 修改的只是指针的 token 序列,两者同名但作用对象完全不同。
版本历史
- Added in version 3.6.0:该 API 首次加入;
- Changed in version 3.11.0:参数
token的类型调整为string_t。这一变化使json_pointer与库的自定义字符串类型(StringType模板参数,兼容std::string、std::wstring等)保持一致,头文件中的string_t定义见 json_pointer.hpp#L56-L58。当前仓库版本为 3.12.0(见 json_pointer.hpp 文件头),上述签名即当前行为。
延伸阅读
同一 API 分组下的相邻文档可帮助补齐 JSON Pointer 的完整操作面:
- json_pointer 类总览
- push_front(在头部插入 token)
- pop_back / pop_front
- back(读取末尾 token)
- empty / to_string
- operator/=(
push_back的运算符封装)
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 StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
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