nlohmann/json 的 basic_json::emplace:向 JSON 对象原地构造并安全插入新成员
【免费下载链接】jsonJSON for Modern C++项目地址: https://gitcode.com/GitHub_Trending/js/json
JSON for Modern C++(nlohmann/json)为basic_json提供了丰富的"修改值"接口,其中emplace()是向JSON 对象按 key 原地构造并插入成员的核心成员函数。本指南以官方 API 文档为骨架,结合 json.hpp 源码实现与 unit-modifiers.cpp 单元测试,系统讲解emplace()的签名、null 自动转换语义、返回的(iterator, bool)判定规则、type_error.311异常边界与ordered_json下的迭代器失效细节,并给出可直接编译运行的完整示例,帮助你准确区分它与insert、push_back、emplace_back、operator[]的适用场景。
函数签名与核心语义
emplace()的完整声明定义于 json.hpp,是basic_json的一个成员模板函数:
template<class... Args> std::pair<iterator, bool> emplace(Args&& ... args);它的工作方式与标准库关联容器的emplace完全一致,但作用对象是basic_json值本身,核心语义为:
- 在JSON 对象上,用给定的
args就地构造(in-place construct)一个新成员,并且仅当容器中不存在相同 key 的元素时才真正插入; - 如果被调用的 JSON 值当前是
#!json null,会先静默创建一个空对象,再向其中追加由args构造出的值; - 若 key 已存在,则不发生任何修改,
args也不会被构造为元素。
对底层的std::map(默认object_t)而言,这意味着emplace避免了"先构造临时basic_json、再由insert拷贝/移动"的中间步骤——构造直接发生在为对象元素分配好的存储空间内。
模板参数与普通参数
| 成员 | 含义 |
|---|---|
模板参数Args | 一组兼容类型,用于构造一个basic_json对象(例如string_t、数值、bool、数组/对象、嵌套basic_json等) |
args(in) | 转发给某个basic_json构造函数的一组实参,通过完美转发(perfect forwarding,Args&& ...)向下传递 |
由于args直接参与basic_json的构造函数重载决议,标准对象构造规则在这里都适用。例如传入(3, "foo")会被解析为构造一个包含 3 个"foo"的数组值——这正是 emplace 与operator[]/insert在构造时机上的差异所在。
返回值:(iterator, bool)双重语义
与标准库关联容器一致,emplace()返回std::pair<iterator, bool>:
first:指向被插入元素的迭代器;如果 key 已存在导致未发生插入,则指向已存在的那个元素(即原值不会被替换);second:#!cpp bool布尔标志,true表示本次确实发生了插入,false表示 key 已存在、未做任何修改。
官方文档 emplace.md 将此表述为"由指向已插入元素或(若未插入)已存在元素的迭代器,与一个表示是否发生插入的布尔值组成的 pair"。在默认object_t = std::map下,判断"已存在"的依据是对象当前使用的比较器(默认字典序比较 key)。
迭代器失效规则
对于普通basic_json(默认底层为std::map),emplace()不会使既有迭代器与引用失效。
但文档特别强调了一种例外:当使用 ordered_json(底层为 ordered_map.hpp 中的ordered_map,本质是基于 vector 的顺序容器)时,向对象追加新成员可能触发重分配(reallocation),此时:
所有迭代器(包括
end()迭代器)以及所有指向既有元素的引用都会失效。
这与 ordered_map.hpp 的实现直接相关:ordered_map::emplace先做一次线性查找判断 key 是否已存在,再调用底层Container::emplace_back把元素追加到末尾,因此一旦容量不足发生扩容,迭代器与引用即全部失效。从源码结构看,这也是ordered_json相比默认std::map在插入语义上最需要留意的差异——前者保序但存在扩容失效,后者保证引用稳定性但按键排序。
异常安全与异常类型
emplace()提供强异常保证(strong guarantee):如果构造过程中抛出异常,任何 JSON 值都不会发生改变——不会出现"对象已从 null 变成 object,但成员没有插入"之类的半成品状态。
type_error.311
当在既非 JSON 对象也非#!json null的值(如数值、数组、字符串、布尔)上调用emplace()时,会抛出json::type_error,异常标识为311,完整定义见 exceptions.md:
[json.exception.type_error.311] cannot use emplace() with number消息中的number会随实际类型名变化(如cannot use emplace() with array)。注意文档明确指出emplace()仅接受object 或 null,这一点与面向数组的emplace_back()(接受 array 或 null)正好互补,两者的类型检查错误共用 311 编号。
对应源码中的守卫逻辑位于 json.hpp:
// emplace only works for null objects or arrays if (JSON_HEDLEY_UNLIKELY(!(is_null() || is_object()))) { JSON_THROW(type_error::create(311, detail::concat("cannot use emplace() with ", type_name()), this)); }若通过,则当值为 null 时,源码会先把内部类型标记与存储区切换为空对象(json.hpp):
// transform a null object into an object if (is_null()) { m_data.m_type = value_t::object; m_data.m_value = value_t::object; assert_invariant(); }复杂度
- 默认
object_t(std::map):对数复杂度 O(log(size())),因为底层是红黑树,插入与查找均为对数时间。 - 需要再次提醒:
ordered_json的底层 ordered_map 使用线性查找 + 末尾追加,实际复杂度是 O(n),官方文档给出的 O(log(size())) 针对的是默认关联容器实现。
完整示例与输出解读
官方示例 emplace.cpp 是学习该 API 的最佳起点,它同时演示了 null 自动转对象、重复 key 不覆盖两个关键行为:
#include <iostream> #include <nlohmann/json.hpp> using json = nlohmann::json; int main() { // create JSON values json object = {{"one", 1}, {"two", 2}}; json null; // print values std::cout << object << '\n'; std::cout << null << '\n'; // add values auto res1 = object.emplace("three", 3); null.emplace("A", "a"); null.emplace("B", "b"); // the following call will not add an object, because there is already // a value stored at key "B" auto res2 = null.emplace("B", "c"); // print values std::cout << object << '\n'; std::cout << *res1.first << " " << std::boolalpha << res1.second << '\n'; std::cout << null << '\n'; std::cout << *res2.first << " " << std::boolalpha << res2.second << '\n'; }编译并运行(使用单头文件版本即可,例如g++ -std=c++11 -I single_include emplace.cpp):
{"one":1,"two":2} null {"one":1,"three":3,"two":2} 3 true {"A":"a","B":"b"} "b" false输出可以逐行对照理解:
- 前两行是初始状态:一个普通对象与一个值为
null的 JSON 值; object.emplace("three", 3)成功后,对象多出"three":3,返回迭代器指向3,second为true;- 两个对
null的emplace("A", "a")、emplace("B", "b")调用把null静默升级成对象{"A":"a","B":"b"}——整个过程无需你预先判断或手工初始化; - 最后
null.emplace("B", "c")因为 key"B"已存在而没有覆盖原值:*res2.first仍打印"b"(原有元素),second打印false。
测试用例对行为的验证
单元测试 unit-modifiers.cpp 的emplace()小节对上述语义做了逐条断言,可作为行为契约来阅读:
- 从 null 起步:默认构造的
json j;依次emplace("foo", "bar")、emplace("baz", "bam"),测试断言res1.second == true、j.type() == json::value_t::object(null 已转型);再emplace("baz", "bad")时断言res3.second == false且*res3.first == "bam"(原值未被替换),最终对象等于{{"baz","bam"},{"foo","bar"}}; - 在已有对象上插入:
json j = {{"foo", "bar"}}上emplace("baz", "bam")成功(second == true),重复插入"foo"失败且既有"bar"保持不变; - 非法类型:
json j = 1;时j.emplace("foo", "bar")抛出异常,断言消息精确匹配[json.exception.type_error.311] cannot use emplace() with number。
与相关接口的分工对照
| 接口 | 适用容器 | 返回值 | 行为要点 |
|---|---|---|---|
emplace() | object / null | pair<iterator, bool> | 按 key 就地构造,key 已存在则不插入不覆盖;null 先转对象 |
emplace_back()(emplace_back.md) | array / null | reference | 就地构造后追加到数组末尾,null 先转数组;自 3.7.0 起返回引用,摊还常数复杂度 |
insert()(insert.md) | array / object | iterator或void | 把已构造好的值按迭代器位置 / key 范围插入 |
operator[]/push_back() | object / array | 引用 /reference | 前者在 key 不存在时默认构造空值并返回引用,可用于赋值 |
从对象语义看,emplace()最接近"键存在即放弃,绝不自作主张覆盖"的原子判空操作,适合实现幂等的成员装配逻辑;而operator[]则会为缺失的 key 就地创建一个空值(通常是null)供随后赋值,两者存在本质差别。更系统的修改场景梳理可参考特性指南 modifying_values.md。
版本历史与兼容性
- 自2.0.8版本起提供
emplace(),签名与语义保持向后兼容; - 源码实现基于类型检查 + null 转型 + 底层容器转发三步完成(见 json.hpp),配合
set_parent处理基于std::unique_ptr的父指针追踪(SAX/串行化子值归属管理)。
使用建议小结
- 需要不覆盖已有键地向对象写入数据、且希望在插入点就地构造值避免多余临时对象时,优先使用
emplace(); - 对
null值调用是安全的,库会自动把它升级为对象,但请在逻辑上意识到这种隐式转型; - 读取返回值时务必同时消费
second:只有它为true时才真正新增了元素; - 遍历或持有
ordered_json的迭代器期间反复调用emplace(),要考虑底层 vector 扩容导致的整体失效; - 不要期望
emplace()能修改已存在键的值——那是operator[]或insert_or_assign类语义的职责。
【免费下载链接】jsonJSON for Modern C++项目地址: https://gitcode.com/GitHub_Trending/js/json
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考