首页
/ JSON for Modern C++ 中 nlohmann::basic_json::emplace_back 解析:原地构造并追加 JSON 数组元素

JSON for Modern C++ 中 nlohmann::basic_json::emplace_back 解析:原地构造并追加 JSON 数组元素

2026-09-06 22:58:11作者:蔡丛锟

本文围绕 JSON for Modern C++(nlohmann/json)的 nlohmann::basic_json::emplace_back 接口展开:它如何以完美转发的方式在数组末尾“原地”构造一个新 JSON 值、在 null 值上调用时如何被隐式转换为数组、以及触发 type_error.311 异常的边界条件。读完本文后,你将能够正确使用 emplace_back 高效追加数组元素,理解其迭代器失效规则,并能在 emplace_backpush_backoperator+= 之间做出恰当选择。

函数签名与行为定义

emplace_back 的完整声明如下(见 API 文档):

template<class... Args>
reference emplace_back(Args&& ... args);

其行为可概括为两点:

  1. 根据传入的参数 args 在 JSON 值的末尾构造一个新的 JSON 值;
  2. 如果该函数被调用在一个 JSON null 值上,会先创建一个空数组,再把由 args 构造出的值追加进去——即 null 会被静默转换为数组,这与 C++ 标准容器“原地构造、避免拷贝”的 emplace 语义一脉相承。

模板参数、参数与返回值

项目 说明
模板参数 Args 可用于构造一个 basic_json 对象的兼容类型(compatible types)
参数 args(in) 将被完美转发给 basic_json 构造函数的实参包
返回值 指向被插入元素的引用(reference)。注意:从 3.7.0 版本起才返回引用,此前版本无返回值

正因为 Args 是转发给 basic_json 的构造函数,参数个数并不固定:单参数如 j.emplace_back(6) 构造一个数字;多参数如 j.emplace_back(3, "second") 则按 basic_json(size_type, string_t) 构造器解释,构造一个含 3 个 "second" 的数组。这一行为在下文的示例中会得到直接验证。

迭代器失效规则

对数组追加元素可能触发底层容器的内存重分配,因此文档给出了明确的迭代器失效约定:

  • 若发生了重分配,则所有迭代器(包括 end())以及所有指向元素的引用全部失效
  • 若未发生重分配,则end() 迭代器失效(参见 end() 文档)。

这意味着持有 j.begin()j[i] 等旧迭代器或引用跨越 emplace_back 调用是未定义行为的来源,尤其在无法预判容量是否耗尽时,应在追加前完成所有基于旧迭代器的遍历。

异常与时间复杂度

  • 异常:当接收者既不是 JSON 数组也不是 null(例如数字、字符串、对象)时,抛出 type_error.311,错误消息形如:

    [json.exception.type_error.311] cannot use emplace_back() with number
    
  • 复杂度:摊还常数(Amortized constant),与 std::vector::emplace_back 一致。

完整示例:向数组追加元素,以及 null 的隐式转换

下面示例来自仓库中的 examples/emplace_back.cpp,完整展示 emplace_back 的典型用法:

#include <iostream>
#include <nlohmann/json.hpp>

using json = nlohmann::json;

int main()
{
    // create JSON values
    json array = {1, 2, 3, 4, 5};
    json null;

    // print values
    std::cout << array << '\n';
    std::cout << null << '\n';

    // add values
    array.emplace_back(6);          // 单参数:追加数字 6
    null.emplace_back("first");     // null 被转换为数组,追加字符串 "first"
    null.emplace_back(3, "second"); // 多参数:构造含 3 个 "second" 的数组

    // print values
    std::cout << array << '\n';
    std::cout << null << '\n';
}

运行输出(与 emplace_back.output 一致):

[1,2,3,4,5]
null
[1,2,3,4,5,6]
["first",["second","second","second"]]

逐行解读:

  • array.emplace_back(6) 把数组扩展为 [1,2,3,4,5,6]
  • nullnull 在第一次 emplace_back 调用后被转换为数组,最终成为 ["first",["second","second","second"]]——第二个元素正是 emplace_back(3, "second")basic_json(size_type count, const string_t&) 语义构造出的三元素数组,直观印证了“参数包被转发给构造函数”这一核心机制。

源码实现剖析

从源码结构看,include/nlohmann/json.hpp 中的实现只有约二十行,逻辑清晰:

template<class... Args>
reference emplace_back(Args&& ... args)
{
    // emplace_back only works for null objects or arrays
    if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_array())))
    {
        JSON_THROW(type_error::create(311, detail::concat("cannot use emplace_back() with ", type_name()), this));
    }

    // transform a null object into an array
    if (is_null())
    {
        m_data.m_type = value_t::array;
        m_data.m_value = value_t::array;
        assert_invariant();
    }

    // add the element to the array (perfect forwarding)
    const auto old_capacity = m_data.m_value.array->capacity();
    m_data.m_value.array->emplace_back(std::forward<Args>(args)...);
    return set_parent(m_data.m_value.array->back(), old_capacity);
}

三个关键实现细节:

  1. 类型守卫:首段 ifJSON_HEDLEY_UNLIKELY 标记为“不太可能发生”的分支,仅允许 is_null() || is_array() 通过,否则经 detail::concat 拼出类型名后抛出 type_error.311——这正是文档中异常条目的来源;
  2. null 到数组的转换:直接把内部 m_data.m_type 改写为 value_t::array 并重置 m_data.m_value,随后 assert_invariant() 校验内部不变量,成本极低;
  3. 完美转发与父节点维护:真正的元素构造委托给内部数组的 std::vector::emplace_back(std::forward<Args>(args)...),随后调用 set_parent 返回新元素引用。

值得注意的是 set_parent(reference j, std::size_t old_capacity)json.hpp):它在开启 JSON_DIAGNOSTICS 诊断时,会比较追加前后的 capacity()——若容量变化(即发生重分配),则调用 set_parents() 全量重建元素间的父子指针。这与文档的迭代器失效规则互为印证:实现层明确感知了“重分配”这一情形。从源码结构看,父指针机制服务于 JSON_DIAGNOSTICSjson_pointer 相关功能,关闭诊断时该路径的额外开销被条件编译裁剪。

测试用例验证

仓库单元测试 tests/src/unit-modifiers.cppSECTION("emplace_back()") 覆盖了三类场景,可作为行为基线:

  • 对 null 值j.emplace_back(1) 后检查返回值 x1 == 1,两次追加后断言 j.type() == json::value_t::arrayj == json({1, 2}),验证了“null 静默转数组”与返回值引用语义;
  • 对已有数组{1,2,3}emplace_back("Hello") 得到 {1, 2, 3, "Hello"}
  • 多参数构造j.emplace_back(3, "foo") 断言返回值为 {"foo","foo","foo"},与官方示例的输出一致;
  • 非法类型:对数字值 json j = 1 调用 j.emplace_back("Hello"),断言抛出 [json.exception.type_error.311] cannot use emplace_back() with number

与 push_back、operator+= 的选型

修改值特性文档 中,emplace_backpush_back 并列为“向数组追加元素”的首选手段。三者的差异在于:

  • push_back 接收已构造好的 basic_json 对象(或对象键值对、初始化列表),并支持向对象插入键值对;其对应异常为 type_error.308,且针对对象插入时复杂度为 O(log n);
  • emplace_back 只在数组语义下工作,直接转发参数原地构造,省去先构造临时对象再移动/拷贝的步骤,异常编号为 type_error.311
  • operator+= 则提供更简写的追加语法。

当需要“按参数直接构造一个新元素并追加”时,emplace_back 是语义最贴切、开销最小的选择;当元素已是现成的 json 值,或目标是 JSON 对象时,应改用 push_back

版本历史

  • 该接口自 2.0.8 版本引入;
  • 3.7.0 版本起返回指向新插入元素的引用(reference),在此之前无返回值。

因此在使用 3.7.0 之后的版本编写依赖返回值引用的代码(如 auto& x = j.emplace_back(...))是安全的;若项目仍停留在更旧版本,则需避免对返回值做引用绑定。

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