nlohmann::basic_json::erase:JSON for Modern C++ 删除元素的五重载全景与源码级解析
在 JSON for Modern C++(nlohmann/json)中,nlohmann::basic_json::erase 是"修改值"(Modifying values)一组成员,用于删除对象成员、数组元素以及通过迭代器定位的任意 JSON 值。它提供 5 个重载:按迭代器删除单元素、按迭代器区间删除、按对象键删除(含 C++17 std::string_view 异构键版本)、按数组下标删除。读完本文,你将掌握每个重载的适用类型、返回值、异常编码(307/202/203/204/205/401)、复杂度与迭代器失效规则,并能在实际项目中写出可复制运行的删除代码。
一、方法签名总览
erase 的 5 个重载声明如下(见 API 参考文档):
// (1) 按迭代器删除单个元素
iterator erase(iterator pos);
const_iterator erase(const_iterator pos);
// (2) 按迭代器区间 [first, last) 删除
iterator erase(iterator first, iterator last);
const_iterator erase(const_iterator first, const_iterator last);
// (3) 按对象键删除
size_type erase(const typename object_t::key_type& key);
// (4) 异构键删除(C++17 下可用 string_view 等可比较键)
template<typename KeyType>
size_type erase(KeyType&& key);
// (5) 按数组下标删除
void erase(const size_type idx);
其中模板参数 KeyType 是一个不同于 json_pointer 的对象键类型,需满足能与 string_t 通过 object_comparator_t 比较;在 C++17 下它也可以是 string view。
二、五个重载的语义逐条解析
(1) 按迭代器删除单个元素
pos 必须有效且可解引用,因此 end()(有效但不可解引用)不能作为 pos。对 null 以外的任何原始类型调用,结果 JSON 值会被置为 null——这是对"删除一个标量/字符串/二进制整体"的语义约定,而不是删除其内部某个字符。
(2) 按迭代器区间删除 [first, last)
若 first == last,first 不必可解引用,删除空区间是一个无操作(no-op)。对 null 以外的原始类型调用,同样会把整个值置为 null。
(3)/(4) 按对象键删除
从 JSON 对象中按键删除成员。重载 (4) 是模板版本,仅在 KeyType 与 typename object_t::key_type 可比较、且 typename object_comparator_t::is_transparent 表示一个类型时才可用(即底层容器支持异构查找),它返回"实际删除的元素个数":默认 std::map 实现下恒为 0(键不存在)或 1(键存在)。
(5) 按数组下标删除
从 JSON 数组中按下标 idx 删除元素,无返回值。仅在数组上可用,idx >= size() 时抛出 out_of_range.401。
三、返回值
| 重载 | 返回值 |
|---|---|
| (1) | 紧随最后一个被删元素之后的迭代器;若 pos 指向最后一个元素,则返回 end() |
| (2) | 紧随最后一个被删元素之后的迭代器;若 last 指向最后元素,则返回 end() |
| (3) | 删除的元素个数;默认 std::map 时恒为 0 或 1 |
| (4) | 同 (3) |
| (5) | 无(void) |
异常安全:提供强异常安全保证(strong exception safety)——若发生异常,原始值保持完整。
四、异常行为对照表
结合 异常文档,5 个重载可能抛出的异常如下:
| 重载 | 异常 | 触发条件 | 典型消息 |
|---|---|---|---|
| (1) | type_error.307 |
对 null 值调用 |
"cannot use erase() with null" |
| (1) | invalid_iterator.202 |
迭代器不属于当前 JSON 值 | "iterator does not fit current value" |
| (1) | invalid_iterator.205 |
对原始类型使用了非 begin() 的迭代器 |
"iterator out of range" |
| (2) | type_error.307 |
对 null 值调用 |
"cannot use erase() with null" |
| (2) | invalid_iterator.203 |
迭代器不属于当前 JSON 值 | "iterators do not fit current value" |
| (2) | invalid_iterator.204 |
对原始类型使用了非 begin()/end() 边界之外的迭代器 |
"iterators out of range" |
| (3)(4) | type_error.307 |
对非对象类型调用 | "cannot use erase() with null" |
| (5) | type_error.307 |
对非数组类型调用 | "cannot use erase() with null" |
| (5) | out_of_range.401 |
idx >= size() |
"array index 17 is out of range" |
五、复杂度
| 重载 | 复杂度 |
|---|---|
| (1) | 对象:摊还常数;数组:与 pos 到容器末尾的距离成线性;字符串/二进制:与成员长度成线性;其他类型:常数 |
| (2) | 对象:log(size()) + std::distance(first, last);数组:与 first 到 last 的距离线性,再加与 last 到末尾的距离线性;字符串/二进制:与成员长度成线性;其他:常数 |
| (3)(4) | log(size()) + count(key) |
| (5) | 与 idx 到容器末尾的距离成线性 |
迭代器失效规则(Notes):
- 重载 (1):会使
erase点处及之后的迭代器与引用失效,包括end()迭代器; - 重载 (2):无特别失效说明;
- 重载 (3)(4):仅被删元素的引用和迭代器失效,其他不受影响;
- 重载 (5):无特别失效说明。
六、可运行示例全收录
以下 5 个示例与 API 文档 一一对应,均基于 示例源文件 可独立编译运行。
示例 (1):按迭代器删除元素
erase__IteratorType.cpp 演示了 erase(iterator) 在 6 种 JSON 类型上的效果:
#include <iostream>
#include <nlohmann/json.hpp>
using json = nlohmann::json;
int main()
{
// create JSON values
json j_boolean = true;
json j_number_integer = 17;
json j_number_float = 23.42;
json j_object = {{"one", 1}, {"two", 2}};
json j_array = {1, 2, 4, 8, 16};
json j_string = "Hello, world";
// call erase()
j_boolean.erase(j_boolean.begin());
j_number_integer.erase(j_number_integer.begin());
j_number_float.erase(j_number_float.begin());
j_object.erase(j_object.find("two"));
j_array.erase(j_array.begin() + 2);
j_string.erase(j_string.begin());
// print values
std::cout << j_boolean << '\n';
std::cout << j_number_integer << '\n';
std::cout << j_number_float << '\n';
std::cout << j_object << '\n';
std::cout << j_array << '\n';
std::cout << j_string << '\n';
}
实际输出(erase__IteratorType.output):
null
null
null
{"one":1}
[1,2,8,16]
null
可以看到:布尔、整数、浮点、字符串被整体置为 null;对象删掉 "two" 键;数组删掉下标 2 的元素 4。
示例 (2):按迭代器区间删除
erase__IteratorType_IteratorType.cpp 演示 erase(first, last):
// call erase()
j_boolean.erase(j_boolean.begin(), j_boolean.end());
j_number_integer.erase(j_number_integer.begin(), j_number_integer.end());
j_number_float.erase(j_number_float.begin(), j_number_float.end());
j_object.erase(j_object.find("two"), j_object.end());
j_array.erase(j_array.begin() + 1, j_array.begin() + 3);
j_string.erase(j_string.begin(), j_string.end());
输出(erase__IteratorType_IteratorType.output):
null
null
null
{"one":1}
[1,8,16]
null
数组中区间 [begin()+1, begin()+3)(即 2, 4)被移除,剩下 [1, 8, 16]。
示例 (3):按对象键删除
erase__object_t_key_type.cpp 演示键删除及返回值:
// create a JSON object
json j_object = {{"one", 1}, {"two", 2}};
// call erase()
auto count_one = j_object.erase("one");
auto count_three = j_object.erase("three");
std::cout << j_object << '\n';
std::cout << count_one << " " << count_three << '\n';
输出(erase__object_t_key_type.output):
{"two":2}
1 0
返回值 1 表示 "one" 存在并被删除,0 表示 "three" 不存在。
示例 (4):C++17 下用 string_view 作为键
erase__keytype.c++17.cpp(版本 3.11.0 起提供)展示了重载 (4) 的异构键能力:
#include <string_view>
#include <nlohmann/json.hpp>
using namespace std::string_view_literals;
using json = nlohmann::json;
int main()
{
json j_object = {{"one", 1}, {"two", 2}};
// 直接用 string_view 字面量作为键
auto count_one = j_object.erase("one"sv);
auto count_three = j_object.erase("three"sv);
std::cout << j_object << '\n';
std::cout << count_one << " " << count_three << '\n';
}
输出与 (3) 相同(erase__keytype.c++17.output):{"two":2} 与 1 0。
示例 (5):按数组下标删除
json j_array = {0, 1, 2, 3, 4, 5};
j_array.erase(2); // 删除下标 2 的元素
std::cout << j_array << '\n';
[0,1,3,4,5]
七、源码级实现解析
以下分析基于 include/nlohmann/json.hpp(单头文件版本,与 single_include/nlohmann/json.hpp 内容一致)。
7.1 迭代器删除:类型分派 + 原始类型整体置空
模板实现 erase(iterator) 的骨架是:
// make sure the iterator fits the current value
if (JSON_HEDLEY_UNLIKELY(this != pos.m_object))
{
JSON_THROW(invalid_iterator::create(202, "iterator does not fit current value", this));
}
先校验迭代器绑定的宿主对象是否为当前 JSON 值,否则抛 invalid_iterator.202——这解释了为什么用"别的 json 对象的迭代器"删除会失败。
随后按 m_data.m_type 分派:
- 布尔/整数/浮点/无符号/字符串/二进制:若
pos不是begin()(primitive_iterator.is_begin()),抛invalid_iterator.205;否则通过分配器释放string/binary内存(L2524-L2537),最后把m_data.m_type改为value_t::null——这就是"原始类型 erase 后变 null"的底层来源; - 对象:转发给内部
std::map的erase(pos),返回其后的迭代器(L2544-L2548); - 数组:转发给内部
std::vector的erase(pos)(L2550-L2554); - null/discarded:抛
type_error.307(L2556-L2559)。
7.2 区间删除:203/204 与空区间 no-op
模板实现 erase(first, last) 要求两个迭代器都绑定到当前对象(this != first.m_object || this != last.m_object 则抛 invalid_iterator.203)。对原始类型,要求 first 必须是 begin() 且 last 必须是 end()(L2588-L2593),否则抛 invalid_iterator.204;这从实现层面保证了 first == last 的空区间只在合法边界上生效(no-op)。对象与数组分支直接转发给底层容器的区间 erase(L2615-L2627)。
7.3 键删除:非模板重载 + SFINAE 内部转发
非模板 erase(key) 有一个值得注意的设计:
size_type erase(const typename object_t::key_type& key)
{
// the indirection via erase_internal() is added to avoid making this
// function a template and thus de-rank it during overload resolution
return erase_internal(key);
}
源码注释明确说明:通过 erase_internal() 间接调用,是为了避免该重载变成模板从而在重载决议中"降权"(de-rank),确保非模板版本优先于模板版本 (4) 被选中。
内部 erase_internal 用 detail::has_erase_with_key_type 做 SFINAE 分派两条路径:
- 若
object_t原生支持该键类型(如默认std::map在 C++14 起的透明比较),直接object->erase(key); - 否则回退为
find+ 单迭代器erase,找到返回1,否则返回0(L2662-L2668)。
两条路径都会先检查 is_object(),非对象一律抛 type_error.307。这正是"返回值恒为 0 或 1"的出处。
7.4 下标删除:仅数组 + 越界 401
void erase(size_type idx) 的逻辑很直白:
if (JSON_HEDLEY_LIKELY(is_array()))
{
if (JSON_HEDLEY_UNLIKELY(idx >= size()))
{
JSON_THROW(out_of_range::create(401,
detail::concat("array index ", std::to_string(idx), " is out of range"), this));
}
m_data.m_value.array->erase(m_data.m_value.array->begin() + static_cast<difference_type>(idx));
}
else
{
JSON_THROW(type_error::create(307, detail::concat("cannot use erase() with ", type_name()), this));
}
它把下标转成 begin() + idx 后交给底层 std::vector::erase,因此复杂度与 idx 到末尾的距离成线性,与"复杂度"一节吻合。
相关测试覆盖在 tests/src/unit-modifiers.cpp(修改类成员的单测集中文件),可用于验证本文各异常与返回值描述的行为。
八、版本历史与相关 API
版本沿革(来自 API 文档):
| 重载 | 版本 |
|---|---|
| (1) | 1.0.0 起提供;3.8.0 增加对二进制类型的支持 |
| (2) | 1.0.0 起提供;3.8.0 增加对二进制类型的支持 |
| (3) | 1.0.0 起提供 |
| (4) | 3.11.0 起提供 |
| (5) | 1.0.0 起提供 |
相关接口:
- clear:清空整个 JSON 值的内容;
- insert:向数组/对象插入值(与
erase方向相反的操作); - Modifying values:修改值的功能专题文章。
九、使用要点小结
- 对原始类型(布尔/数字/字符串/二进制),
erase(begin())或erase(begin(), end())的效果是把整个值置为null,而不是"删掉一个字符"; - 对
null值调用任何重载都会抛type_error.307,删除前建议先用is_null()防护; - 按对象键删除返回值是删除个数(0/1),适合判断键是否实际存在;C++17 下可直接传
string_view(重载 4,3.11.0+)避免临时std::string构造; - 数组按下标删除是
void返回且线性移动尾部元素,频繁头部删除时优先考虑其他数据结构或使用erase(iterator)形式; - 所有重载都提供强异常安全保证,出错时原值保持完整。
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 StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00