JSON for Modern C++(nlohmann/json)值修改完全指南:push_back、emplace、update 与 erase 实战
【免费下载链接】jsonJSON for Modern C++项目地址: https://gitcode.com/GitHub_Trending/js/json
创建 JSON 值只是第一步,实际开发中绝大多数工作是“修改”:向数组追加元素、向对象插入或替换成员、将两个对象合并、按需删除或清空数据。本文以 nlohmann/json(JSON for Modern C++)官方文档 modifying_values 为主线,系统讲解对已存在 JSON 值进行增、改、并、删的全部核心 API:push_back/emplace_back、operator[]/emplace、update、erase/clear。读完本文,你将掌握每个修改接口的调用方式、null值的隐式类型转换规则、复杂度与异常语义,以及递归合并、按“不存在才插入”实现等真实工程场景的推荐写法。
修改操作的全景:增、改、并、删
nlohmann/json 把 JSON 值建模为nlohmann::basic_json(通常简写为json),对已存在值的修改可分为四类:
| 操作 | 面向类型 | 主要 API | 语义 |
|---|---|---|---|
| 追加/新增 | 数组 | push_back、emplace_back、operator+= | 末尾追加元素 |
| 插入/替换 | 对象 | operator[]、emplace、push_back | 插入或整体覆盖键 |
| 合并 | 对象 | update、JSON Merge Patch | 把另一对象的成员拷入并覆盖 |
| 删除/清空 | 全部 | erase、clear | 移除成员/元素或清空值 |
对于只读访问(at、find、value等),请参见官方文档 element access,本文聚焦可变操作。所有修改接口都以json&方式就地作用于调用对象,无需重新赋值(除少数显式重载外)。
贯穿全文的一个重要设计:null的隐式转换。未初始化的json j;其类型是null。库中多数“插入类”函数在检测到调用目标是null时,会先把它就地转换成对应容器(数组或空对象)再执行插入,因此可以用一条语句从零开始构建结构(见下文各节)。
向数组追加元素:push_back 与 emplace_back
基本用法与 operator+=
向数组追加元素最直接的方式是push_back。从官方示例可以看到null到数组的自动转换:
json j; // null j.push_back(1); // [1] j.push_back(2); // [1,2] j.emplace_back(3); // [1,2,3] // operator+= is a shorthand for push_back j += 4; // [1,2,3,4]j初始为null,第一次push_back(1)时被隐式转换为[ ]再追加元素,所以不必先手动j = json::array()。operator+=等价于push_back,是追加标量/值的最紧凑写法。
对应完整可运行示例可在 examples/push_back.cpp 中找到,其渲染输出见 examples/push_back.output。
push_back 的三组重载
从 API 文档 push_back.md 看,push_back实际有 3 组重载:
// (1) 追加元素到数组末尾 void push_back(basic_json&& val); void push_back(const basic_json& val); // (2) 把对象元素插入 JSON 对象(object_t::value_type 即 key-value 对) void push_back(const typename object_t::value_type& val); // (3) 用初始化列表调用 void push_back(initializer_list_t init);值得说明的是重载 (3):当当前值是对象、初始化列表恰好含两个元素、且第一个元素是字符串时,该列表会被解释为一个对象的键值对插入;否则被转换为普通 JSON 值按数组语义追加。这种二义性处理源自历史 issue(见库注释),目的是让{{"key", "value"}}之类写法能正确落到对象语义上。对应示例见 examples/push_back__object_t__value.cpp 与 examples/push_back__initializer_list.cpp。
emplace_back:就地构造
emplace_back不要求先构造好basic_json,而是直接把可变参数args转发给basic_json的某个构造函数,在数组末尾就地构造并返回新元素的引用:
template<class... Args> reference emplace_back(Args&& ... args);这在需要追加“由多个来源组装的值”时能省去一次临时对象拷贝/移动。例如追加一个由初始化列表构造的嵌套对象:j.emplace_back(json{{"x", 1}, {"y", 2}})。若调用目标为null,同样先转为空数组。自 3.7.0 起该函数返回被插入元素的引用(更早版本无返回值),见 emplace_back.md。
复杂度与异常(数组语义):
| 接口 | 复杂度 | 非法类型抛出 |
|---|---|---|
push_back(1)/(3) 追加到数组 | 平摊常数 | type_error.308:"cannot use push_back() with <type>" |
push_back(2) 插入对象 | O(log(size())) | type_error.308 |
emplace_back | 平摊常数 | type_error.311:"cannot use emplace_back() with <type>" |
这些异常码与消息在实现中被集中抛出,例如 include/nlohmann/json.hpp 中JSON_THROW(type_error::create(308, ...))。也就是说,对number、string等非数组也非null的值调用追加接口会直接抛出type_error,而不是“自动转型”。
迭代器失效规则(值得在循环中留意):向数组追加可能引发重新分配(reallocation),此时全部迭代器(含end())与元素引用均失效;若未发生重分配,仅end()失效。对于使用ordered_json的场景,向对象追加成员也可能触发重新分配并使所有迭代器、引用失效(见 push_back.md 与 emplace_back.md)。若需在遍历的同时插入,建议先收集再批量修改,或使用索引访问。
向对象添加与替换成员:operator[] 与 emplace
operator[]:插入即替换
对象修改最常用的方式是operator[]:键不存在则插入,存在则整体替换对应值,这是官方的推荐入口:
json j; j["name"] = "Mary"; // {"name":"Mary"} j["name"] = "John"; // {"name":"John"} (replaced)operator[]同时服务于“读取 + 写入”(对不存在的键做写访问会创建一个值为null的新成员),其完整语义见 API 文档 operator[]。这种“插入或覆盖”语义适合大多数场景,但注意它总是整体替换:如果旧值是一个对象而新值是标量,旧对象内容会被直接丢弃,不会逐层合并。
emplace:仅当键缺失时插入(add-if-absent)
如果业务需求是“只在键不存在时写入,已存在则跳过”,应使用emplace:
template<class... Args> std::pair<iterator, bool> emplace(Args&& ... args);它把args就地构造成一个对象成员,仅当容器中尚不存在该键才插入,返回值中bool表示是否真的发生了插入,iterator指向新插入元素(或已存在的同名元素):
json j; auto [it, inserted] = j.emplace("a", 1); // inserted == true auto [it2, inserted2] = j.emplace("a", 2); // inserted2 == false, "a" 仍为 1这是实现“合并时不覆盖用户显式设置”之类语义的便捷工具。若调用目标为null,会先隐式转为空对象。相关特性:复杂度 O(log(size()));异常时提供强异常保证(strong guarantee,抛出异常则任何 JSON 值都不变);对非对象或null以外的类型调用抛出type_error.311("cannot use emplace() with number")。从源码结构看,对象成员实际存储于object_t(默认为std::map<std::string, json>)中,因此emplace/按键插入的时间复杂度为 O(log n),与std::map的插入语义一致。
合并对象:update 的浅合并与递归深合并
合并两个对象是配置文件合并、默认参数合并中最常见的操作。nlohmann/json 为此提供update,其语义受 Python 的dict.update启发——把另一个对象的所有成员拷入,重复键默认被覆盖。
两种签名
// (1) 从另一个 JSON 对象合并 void update(const_reference j, bool merge_objects = false); // (2) 从同一 JSON 对象上的迭代器区间 [first, last) 合并 void update(const_iterator first, const_iterator last, bool merge_objects = false);实现上,重载 (1) 只是把参数转发给重载 (2):update(j.begin(), j.end(), merge_objects),见 include/nlohmann/json.hpp。
merge_objects=false 与 =true 的差别
merge_objects = false(默认):源对象中已存在的键被整体覆盖(浅合并)。merge_objects = true:当源对象中某键的值是对象,且目标对象中该键的已有值也是对象时,对该键递归执行合并;其余情况(值非对象、键不存在、已有值非对象)仍按覆盖处理。
官方示例 examples/update.cpp 同时演示了两种模式。它先构造两个对象:
json o1 = R"( {"color": "red", "price": 17.99, "names": {"de": "Flugzeug"}} )"_json; json o2 = R"( {"color": "blue", "speed": 100, "names": {"en": "plane"}} )"_json; json o3 = o1; // add all keys from o2 to o1 (updating "color", replacing "names") o1.update(o2); // add all keys from o2 to o1 (updating "color", merging "names") o3.update(o2, true);输出(examples/update.output)清晰展现了差异:
{ "color": "blue", "names": { "en": "plane" }, "price": 17.99, "speed": 100 } { "color": "blue", "names": { "de": "Flugzeug", "en": "plane" }, "price": 17.99, "speed": 100 }第一个输出中names被o2的值整体替换(de丢失);第二个输出中names被递归合并,de与en共存。
实现机理:何时才递归
update的合并逻辑可以在 include/nlohmann/json.hpp 中看到:
for (auto it = first; it != last; ++it) { if (merge_objects && it.value().is_object()) { auto it2 = m_data.m_value.object->find(it.key()); // Only recurse when the existing value is itself an object. // Otherwise overwrite, matching the documented "all other values // are overwritten as usual" behavior (see #5402). if (it2 != m_data.m_value.object->end() && it2->second.is_object()) { it2->second.update(it.value(), true); // ... JSON_DIAGNOSTICS 下维护 m_parent continue; } } m_data.m_value.object->operator[](it.key()) = it.value(); }两个细节值得注意:
- 递归发生的前提是两端同键的值都必须是对象;若已有值不是对象,即使源值是对象也会整体覆盖,这与官方文档注释(#5402)描述的“all other values are overwritten as usual”一致。
- 若目标对象中该键不存在,则直接插入源值(含嵌套对象),不会无谓递归。
此外,update被调用在一个null值上时,会先把null转为空对象(见 include/nlohmann/json.hpp),这与push_back的null转换策略保持一致。
工程场景:默认配置与用户配置合并
这是 update.md 给出的典型用例。应用默认设置如下:
{ "color": "red", "active": true, "name": {"de": "Maus", "en": "mouse"} }用户选择性覆盖:
{ "color": "blue", "name": {"es": "ratón"} }先浅合并再深合并分别得到:
auto user_settings = json::parse("config.json"); auto effective_settings = get_default_settings(); effective_settings.update(user_settings); // 默认浅合并,重复键整体覆盖 // effective_settings.update(user_settings, true); // 深合并,对象键逐层合并- 默认合并结果:
"name"被整体替换为{"es": "ratón"},de/en丢失,active因用户未设置而保留。 - 深合并结果(
merge_objects = true):"name"变为{"de": "Maus", "en": "mouse", "es": "ratón"}。
update 的边界条件与语义摘要
- 类型约束:只能在对象上调用;对非对象抛出
type_error.312("cannot use update() with string")。区间版额外要求first与last属于同一 JSON 对象,否则抛出invalid_iterator.210("iterators do not fit")。 - 复杂度:两种重载均为 O(N·log(size()+N)),N 为待插入元素个数。
- 异常安全:basic guarantee——若中途抛出异常,值可能被部分修改。
- 版本:
update自 3.0.0 加入;merge_objects参数在 3.10.5 引入。 - 使用
ordered_json时,向对象添加成员可能触发重新分配,使全部迭代器与引用失效。
update 与其他合并机制的边界
update负责“把另一个完整对象合进来”。如果你需要的是结构化的差异修改,nlohmann/json 还提供两条更规范的路径,官方文档分别有独立专题(详见文末延伸阅读):
- JSON Merge Patch(RFC 7386):把“待修改补丁”整体应用到一个文档上,天然是递归合并语义,与
update(obj, true)的应用目标互补;patch 中显式置null表示删除键。 - JSON Patch(RFC 6902):用一系列明确定义的编辑操作(
add/remove/replace/move/copy/test)配合json_pointer定位并修改文档。
删除元素:erase 的多种形态
删除元素由erase完成,它按调用目标与参数形态共有 5 组重载:
// (1) 按迭代器删除单个元素 iterator erase(iterator pos); const_iterator erase(const_iterator pos); // (2) 按迭代器区间删除 [first, last) iterator erase(iterator first, iterator last); // (3) 按键删除对象成员 size_type erase(const typename object_t::key_type& key); // (4) 透明比较器按键删除(3.11.0+,可与 string_view 等键类型比较) template<typename KeyType> size_type erase(KeyType&& key); // (5) 按下标删除数组元素 void erase(const size_type idx);官方示例给出最常用的两种形态:
json j = {{"a", 1}, {"b", 2}, {"c", 3}}; j.erase("b"); // {"a":1,"c":3} json a = {1, 2, 3, 4}; a.erase(1); // [1,3,4] (erase by index)各形态的关键语义
- 按键 (3)/(4):仅对象可用;返回值是实际删除的元素个数(默认
object_t为std::map时恒为 0 或 1)。透明比较版本允许传入std::string_view之类的键类型而避免临时std::string构造(C++17)。 - 按下标 (5):仅数组可用;复杂度与被删元素到数组末尾的距离成线性(后续元素需前移);下标越界(
idx >= size())抛出out_of_range.401,如"array index 17 is out of range"。 - 按迭代器 (1)/(2):可用在数组、对象等上;
pos必须是有效且可解引用的迭代器(不能是end())。特别地:如果在除null外的原始类型(primitive,如number/string)上调用迭代器版,值会被置为null——这是该接口为保持统一返回迭代器语义而做出的行为,使用时需留意。 - 对
null调用任何形态都会抛出type_error.307("cannot use erase() with null");迭代器不属于当前值则抛invalid_iterator系列异常(202/203/204/205)。 - 异常安全:strong guarantee,异常时原值保持完好。
- 迭代器失效:对象按迭代器/键删除会失效被删元素相关的引用与迭代器;数组按迭代器删除会使被删位置及之后的迭代器、引用(含
end())失效。
对应可运行示例包括 examples/erase__IteratorType.cpp、examples/erase__object_t_key_type.cpp 与 examples/erase__size_type.cpp。
清空但保留类型:clear
clear的语义与erase不同:它清空内容但保留当前 JSON 类型,并把值重置为该类型的“默认值”,等价于*this = basic_json(type()):
void clear() noexcept;| 当前类型 | clear 后的值 |
|---|---|
| null | null |
| boolean | false |
| string | "" |
| number | 0 |
| binary | 空字节数组 |
| object | {} |
| array | [] |
该函数声明为noexcept(实现见 single_include/nlohmann/json.hpp 对应入口),官方文档标注其具有不抛异常保证(no-throw guarantee),复杂度为 O(size)。注意它会失效与该值相关的全部迭代器、指针与引用。典型用途是把已用过的数组/对象“掏空复用”而不改变其在父结构中的位置与类型;例如清空一个日志缓冲数组使其继续保持数组类型、可被再次push_back。
如何把这些修改接口组合使用
综合示例(可直接编译运行):
#include <iostream> #include <nlohmann/json.hpp> using json = nlohmann::json; int main() { json doc = json::object(); // 显式创建对象 // 数组:从 null 起步逐条追加 json tags; tags.emplace_back("cpp"); tags += "json"; doc["tags"] = std::move(tags); // 移入,避免拷贝 // 对象:仅在缺失时插入默认值 doc.emplace("retries", 3); // 合并:把一段用户覆盖合入默认配置(深合并) json defaults = {{"retries", 5}, {"name", {"en", "default"}}}; doc.update(defaults, false); // 已有键被覆盖 doc.update(R"({"name":{"zh":"示例"}})"_json, true); // 递归合并 name // 删除与清空 doc.erase("retries"); // 按键删除 doc["tags"].clear(); // 清空但保持数组类型 std::cout << doc.dump(2) << '\n'; // 输出剩余结构 }工程实践中几条建议:
- 构建大数组时优先
emplace_back/+=而非反复operator[],代码更贴近“顺序追加”语义; - “add if absent”务必用
emplace而非先查后写,既省一次查找又天然线程内原子(单线程下); - 合并配置时先想清楚要浅合并还是深合并,
update第二参数merge_objects是浅/深的分水岭; - 若要长期持有对成员对象的引用(如
auto& x = j["cfg"];),后续对该父对象执行插入/删除前要意识到可能引发的失效; - 大范围的批量差异修改,优先考虑 JSON Merge Patch 与 JSON Patch,而不是手写多行
erase+update。
延伸阅读
- 创建值:创建对象、数组、字面量
_json等基础见 creating_values; - 只读取值:见 element access 与 basic_json API 总览;
- 各接口 API 参考:push_back、emplace_back、emplace、update、erase、clear;
- 结构化批量修改:JSON Merge Patch 与 JSON Patch & Diff、json_pointer;
- 修改类接口的实现集中位于 include/nlohmann/json.hpp(
update/erase/clear同处该文件),单头版本见 single_include/nlohmann/json.hpp;相关单元测试可在 tests/src/unit-modifiers.cpp 中探索。
【免费下载链接】jsonJSON for Modern C++项目地址: https://gitcode.com/GitHub_Trending/js/json
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考