JSON for Modern C++ 中 `std::swap<basic_json>` 的用法与实现原理
basic_json 是 JSON for Modern C++(nlohmann/json)中代表任意 JSON 值的核心类。本文基于官方 API 文档页面 std_swap.md,系统讲解如何利用标准库交换惯用法在常数时间内、不抛异常地交换两个 JSON 值,并结合仓库内源码、示例与单元测试剖析其底层实现与工程价值。读完本文,你将掌握 swap 的成员/非成员重载全貌、无异常(noexcept)承诺的来源、ADL 查找机制,以及它在容器排序、拷贝交换惯用法等实战场景中的正确使用方式。
函数签名与核心语义
std::swap<basic_json> 为 nlohmann::basic_json 提供了一次性交换两个 JSON 值的重载。官方文档给出的声明如下:
namespace std {
void swap(nlohmann::basic_json& j1, nlohmann::basic_json& j2);
}
其中:
j1(in, out):要被j2的值替换的参数;j2(in, out):要被j1的值替换的参数。
交换的语义是“交换内部内容”,而不是元素级逐项搬移:调用结束后 j1 持有原 j2 的整个 JSON 值,j2 持有原 j1 的整个 JSON 值,且两侧原有的迭代器与引用都保持有效,仅“past-the-end”迭代器失效(此点与成员重载族的文档一致,详见 swap.md)。
特别值得注意:JSON 值本身的类型不同并不妨碍交换。对象、数组、字符串、数字、布尔、null、二进制值在运行时是同一 basic_json 的不同状态,因此任意两个 JSON 值之间都能直接交换类型与内容,这正是它区别于下述按底层容器类型专门化重载(array_t&、object_t& 等)的地方。
异常安全与时间复杂度
文档对这一重载给出了两条极其明确的保证:
- No-throw guarantee:该函数在任何情况下都不抛异常。
- Complexity:常数时间
O(1)。
这两条保证是选择 swap 而非“拷贝再赋值”的关键动因——拷贝一个深层 JSON 树既可能分配内存、也可能抛 std::bad_alloc,而交换只是内部存储指针/类型的互换。结合下方源码可以看到,库为这个不抛异常承诺所做的工作比表面复杂得多(见“实现原理”小节),其中涉及对 value_t、json_value 等内部类型可否无异常移动/赋值的编译期推导。
官方给出的“可能实现”与真实实现对照
文档页面给出了最简单的参考实现:
void swap(nlohmann::basic_json& j1, nlohmann::basic_json& j2)
{
j1.swap(j2);
}
即非成员版本直接转调成员版本。在仓库源码中可以找到与之呼应的真实定义:非成员自由函数 swap 位于 include/nlohmann/json.hpp:
NLOHMANN_BASIC_JSON_TPL_DECLARATION
inline void swap(nlohmann::NLOHMANN_BASIC_JSON_TPL& j1, nlohmann::NLOHMANN_BASIC_JSON_TPL& j2) noexcept(
is_nothrow_move_constructible<nlohmann::NLOHMANN_BASIC_JSON_TPL>::value&&
is_nothrow_move_assignable<nlohmann::NLOHMANN_BASIC_JSON_TPL>::value)
{
j1.swap(j2);
}
这里有两处值得留意的实现细节:
- 并非直接写死在
namespace std内:库实际上把swap定义为与basic_json同命名空间的自由函数重载,并通过 ADL(实参依赖查找) 让using std::swap; swap(j1, j2);这样的标准写法能命中它。文档页“namespace std”的写法描述的是它对标准交换惯用法的兼容语义(用户无需关心具体重载位于哪个命名空间)。 noexcept是有条件的:它要求该basic_json特化类型满足“可无异常移动构造且可无异常移动赋值”,因此只要底层存储类型具备相应性质,交换即不抛异常。由于basic_json的非成员/成员swap使用NLOHMANN_BASIC_JSON_TPL模板参数而非写死json,这一保证对任意用户自定义的basic_json<...>特化同样成立——这正是文档“Version history”中 3.10.5 扩展到任意 basic_json 类型的由来(早期仅针对默认类型实例化)。
完整可运行示例与输出
文档引用的示例源码是 examples/std_swap.cpp,其中演示了在对象与数组这两种异构类型之间直接交换:
#include <iostream>
#include <iomanip>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
// create JSON values
json j1 = {{"one", 1}, {"two", 2}};
json j2 = {1, 2, 4, 8, 16};
std::cout << "j1 = " << j1 << " | j2 = " << j2 << '\n';
// swap values
std::swap(j1, j2);
std::cout << "j1 = " << j1 << " | j2 = " << j2 << std::endl;
}
编译方式与普通 nlohmann/json 单头文件用法完全一致(例如 g++ -std=c++11 -I single_include std_swap.cpp),因为库在 single_include/nlohmann/json.hpp 提供直接可包含的头文件。程序运行输出如下(见 examples/std_swap.output):
j1 = {"one":1,"two":2} | j2 = [1,2,4,8,16]
j1 = [1,2,4,8,16] | j2 = {"one":1,"two":2}
可以看到:交换前 j1 是含两个键值对的对象、j2 是含五个元素的数组;交换后二者的类型与内容整体互换。这个例子也直观证明了“非同构 JSON 亦可交换”这一特点——basic_json 以带类型标签的联合体存储任意 JSON 结构,交换只发生在顶层存储状态层面。
实战注意:如何保证命中高效的 swap 重载
虽然上例直接写 std::swap(j1, j2) 即可工作,但在泛型代码里更稳妥的写法是结合 using std::swap 的惯用法,让编译器通过 ADL 优先找到上面那个 O(1) 的专属重载:
json a = {{"name", "nlohmann"}, {"stars", 1}};
json b = {{"lang", "C++"}};
using std::swap; // 引入标准库的 std::swap
swap(a, b); // ADL 命中 nlohmann 命名空间中的专属重载,O(1)
这与仓库单元测试 tests/src/unit-modifiers.cpp 中 “nonmember swap” 一节的做法完全一致:
json j("hello world");
json k(42.23);
using std::swap;
swap(j, k);
CHECK(j == json(42.23));
CHECK(k == json("hello world"));
深入:完整的 swap 重载家族与类型约束
本文档对应的成员版本与各类型专门化重载,在 include/nlohmann/json.hpp 中集中定义,并在 swap.md 中逐一说明。全套接口为:
| 重载 | 用途 | 是否可为空 JSON 之外的类型调用 |
|---|---|---|
void swap(reference other) |
交换整个 JSON 值 | 任意 JSON 值均可 |
friend void swap(reference left, reference right) |
通过 ADL 可调用的非成员交换 | 任意 JSON 值均可 |
void swap(array_t& other) |
与外部数组容器交换 | 仅当 is_array(),否则抛 type_error.310 |
void swap(object_t& other) |
与外部对象容器交换 | 仅当 is_object(),否则抛 type_error.310 |
void swap(string_t& other) |
与外部字符串交换 | 仅当 is_string(),否则抛 type_error.310 |
void swap(binary_t& other) |
与外部二进制值交换 | 仅当当前值是二进制,否则抛 type_error.310 |
void swap(typename binary_t::container_type& other) |
与裸字节容器交换(不涉及 subtype) | 同上 |
需要强调的是,类型专门化重载(3)–(7)并不带 noexcept,因为它们可能触发与具体容器实现相关的行为;且类型不匹配时统一抛出带编号的异常 [json.exception.type_error.310]。对应抛错路径在源码中清晰可见,例如:
void swap(array_t& other) // ...
{
// swap only works for arrays
if (JSON_HEDLEY_LIKELY(is_array()))
{
using std::swap;
swap(*(m_data.m_value.array), other);
}
else
{
JSON_THROW(type_error::create(310, detail::concat("cannot use swap(array_t&) with ", type_name()), this));
}
}
单元测试 tests/src/unit-modifiers.cpp 验证了这一行为:
// 数组类型正确时的双向交换……
// 类型不匹配时:
CHECK_THROWS_WITH_AS(j.swap(a), "[json.exception.type_error.310] cannot use swap(array_t&) with number", json::type_error&);
而对象、字符串、二进制版本的同类约束分别见 tests/src/unit-modifiers.cpp,错误消息模式均为 cannot use swap(...) with <实际类型名>。
实现原理:为什么交换是 O(1) 且不抛异常
阅读成员版本真实实现(include/nlohmann/json.hpp,对应 single_include 中的 同位置实现):
void swap(reference other) noexcept (
std::is_nothrow_move_constructible<value_t>::value &&
std::is_nothrow_move_assignable<value_t>::value &&
std::is_nothrow_move_constructible<json_value>::value &&
std::is_nothrow_move_assignable<json_value>::value)
{
std::swap(m_data.m_type, other.m_data.m_type);
std::swap(m_data.m_value, other.m_data.m_value);
set_parents();
other.set_parents();
assert_invariant();
}
从源码结构可以看出三个关键点:
basic_json的内部状态由“类型标签 + 值联合体”构成:m_type记录当前 JSON 的类型(value_t枚举,位于 include/nlohmann/detail/value_t.hpp),m_value是承载对象/数组/字符串/二进制等底层存储的联合体json_value。交换只需互换这两个成员,因此复杂度恒为O(1),与 JSON 内容的嵌套深度、元素个数完全无关。noexcept来自对内部类型性质的编译期推导:只有当value_t与json_value均满足“无异常移动构造/赋值”时才声明noexcept。文档所说的“never throws”基于库默认的json实例化(其底层为std::map/std::vector/std::string等标准容器,移动均不抛异常)而成立。- 在开启
JSON_DIAGNOSTICS时维护父指针:诊断模式下每个节点带有m_parent指针,交换后需要调用set_parents()重新修正父子关系,最后用assert_invariant()校验内部不变量。这说明默认看似“一行 std::swap”的交换,实际上在库内部承担了完整的不变量维护责任。
典型工程价值
由于上述 O(1) 且无异常的性质,swap 在以下场景尤为常用:
- 容器与排序算法:对
std::vector<json>、std::priority_queue<json>等执行std::sort等操作时,算法内部依赖高效的元素交换,O(1) 的swap能显著优于整体拷贝; - 拷贝交换惯用法(copy-and-swap):自定义 RAII 包装类时,可用
basic_json的交换实现强异常安全——先拷贝构造临时对象,再与其交换,旧资源由临时对象析构释放; - 原地重组数据结构:在解析、归并、路由转发等对吞吐敏感的高频逻辑中,用交换代替拷贝可避免深层树结构的内存复制。
版本演进与兼容性
文档 “Version history” 提供两条版本信息:
- 1.0.0 起提供:交换能力从库的第一个正式版本就存在,属于长期稳定的基础 API;
- 3.10.5 扩展至任意 basic_json 类型:配合 include/nlohmann/json.hpp 中使用
NLOHMANN_BASIC_JSON_TPL模板参数定义自由函数 swap 的实现方式,使自定义对象映射类型(object_t)、字符串类型(string_t)、分配器等被替换后的特化类型同样能获得专属的高效交换重载。
若希望继续深入,可进一步阅读 swap.md(同一主题的成员/重载族文档)、源码实现 include/nlohmann/json.hpp,或结合头文件 include/nlohmann/detail/value_t.hpp 理解 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 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