首页
/ JSON for Modern C++ 中 `std::swap<basic_json>` 的用法与实现原理

JSON for Modern C++ 中 `std::swap<basic_json>` 的用法与实现原理

2026-09-07 13:50:12作者:胡唯隽

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_tjson_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);
}

这里有两处值得留意的实现细节:

  1. 并非直接写死在 namespace std:库实际上把 swap 定义为与 basic_json 同命名空间的自由函数重载,并通过 ADL(实参依赖查找)using std::swap; swap(j1, j2); 这样的标准写法能命中它。文档页“namespace std”的写法描述的是它对标准交换惯用法的兼容语义(用户无需关心具体重载位于哪个命名空间)。
  2. 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();
}

从源码结构可以看出三个关键点:

  1. basic_json 的内部状态由“类型标签 + 值联合体”构成m_type 记录当前 JSON 的类型(value_t 枚举,位于 include/nlohmann/detail/value_t.hpp),m_value 是承载对象/数组/字符串/二进制等底层存储的联合体 json_value。交换只需互换这两个成员,因此复杂度恒为 O(1),与 JSON 内容的嵌套深度、元素个数完全无关。
  2. noexcept 来自对内部类型性质的编译期推导:只有当 value_tjson_value 均满足“无异常移动构造/赋值”时才声明 noexcept。文档所说的“never throws”基于库默认的 json 实例化(其底层为 std::map/std::vector/std::string 等标准容器,移动均不抛异常)而成立。
  3. 在开启 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 类型标签系统。

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

项目优选

收起
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
898
5.82 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
921
1.84 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.8 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
531
596
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.02 K
519
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.36 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
391