深入理解 nlohmann::json::size():JSON 值的元素个数语义、源码实现与使用陷阱
【免费下载链接】jsonJSON for Modern C++项目地址: https://gitcode.com/GitHub_Trending/js/json
size() 是 nlohmann (JSON for Modern C++) 中用于获取 JSON 值“元素个数”的核心容量接口。本文以 官方 API 文档 size.md 为主线,结合仓库内头文件 include/nlohmann/json.hpp、官方示例 size.cpp 与单元测试 unit-capacity.cpp,系统讲解其返回值规则、底层实现、复杂度保证以及字符串与二进制等最容易踩坑的语义区别,帮助你写出类型安全、行为正确的 JSON 容量判断代码。
接口签名与基本语义
size()是nlohmann::basic_json类上的公有成员函数,声明如下:
size_type size() const noexcept;其语义非常明确:返回一个 JSON 值中“元素”的个数。其中size_type即无符号整数类型(默认映射为std::size_t,可通过模板参数SizeType定制)。
需要特别强调的是,这里的“个数”并不是字节数、字符数或数组底层容量,而是对 JSON 的七种值类型(null、boolean、string、number、binary、object、array)分别给出的统一定义。官方文档对这一函数归属的定位是 capacity 类接口,与empty()、max_size()等构成一组互补的“容量查询”工具。
按值类型划分的返回值规则
官方文档给出了明确的返回值对照表,这是理解size()的关键,完整列举如下:
| JSON 值类型 | size()返回值 |
|---|---|
| null | 0 |
| boolean | 1 |
| string | 1 |
| number | 1 |
| binary | 1 |
| object | 委托调用object_t::size()的结果(即键值对个数) |
| array | 委托调用array_t::size()的结果(即数组元素个数) |
可见其设计思路非常清晰:只有数组和对象是“容器型”值,其余所有标量类型(布尔、字符串、数字、二进制字节串)都被视作一个整体单元,其“元素个数”恒为1,而null则代表“空值”,个数为0。
这里的object_t与array_t是basic_json的两个可定制模板容器类型。在默认配置下:
object_t基于有序/无序键控容器(默认使用std::map<std::string, basic_json, std::less<>>之类的比较器),定义见 include/nlohmann/json.hpp 中的类型别名,详见 object_t 文档;array_t默认是std::vector<basic_json>,定义见 array_t 文档。
因此size()对 object 和 array 的结果分别等于对象键值对个数与数组元素个数,例如{"one": 1, "two": 2}返回2,[1, 2, 4, 8, 16]返回5。当你通过basic_json模板参数自定义容器时,返回值将自动跟随底层容器的size()语义。
源码实现剖析:一次针对 value_t 的 switch 分发
size()的实现并非逐类型手工计算,而是先读取当前 JSON 值内部的类型标记m_data.m_type(枚举类型value_t),再通过一次switch完成分发。相关实现位于 include/nlohmann/json.hpp#L3016-L3051,关键代码如下:
size_type size() const noexcept { switch (m_data.m_type) { case value_t::null: { // null values are empty return 0; } case value_t::array: { // delegate call to array_t::size() return m_data.m_value.array->size(); } case value_t::object: { // delegate call to object_t::size() return m_data.m_value.object->size(); } case value_t::string: case value_t::boolean: case value_t::number_integer: case value_t::number_unsigned: case value_t::number_float: case value_t::binary: case value_t::discarded: default: { // all other types have size 1 return 1; } } }从源码可以提炼出几个有意思的实现事实:
- 统一存储、统一分发:
basic_json采用“类型标记 + 联合式存储”的经典设计,m_type决定了当前解释m_value(union)中哪个成员,size()只关心类型,不关心具体数值内容。 - 对象与数组是仅有的委托路径:
case value_t::array与case value_t::object直接把调用转发给底层容器,从而天然支持自定义容器类型,返回值、复杂度行为都与底层容器一致。 - number 内部三种变体一视同仁:
number_integer、number_unsigned、number_float在实现上虽然分属不同的内部存储成员,但在size()语义上都归于“标量”,统一返回1。 - 内部类型
discarded也返回 1:value_t::discarded是一个仅供内部使用、表示“解析中被丢弃”的类型(如json_lines场景下被过滤的片段)。从文档表格看它不会出现,但从实现看它同样落入“其余类型恒为 1”的分支。普通用户一般不会直接遇到可访问的 discarded 值,属于实现细节。
此外,size()的实现与它的邻居们紧密呼应:紧邻其上是empty()(null 返回 true、标量与非空容器返回 false),紧邻其下是max_size()(见 include/nlohmann/json.hpp#L3055-L3085,对数组与对象委托array_t::max_size()/object_t::max_size(),对其余类型直接返回size())。三者共同构成一套完备的容量查询接口。
最容易踩坑的语义陷阱:string 与 binary
size()是新手最容易产生误解的接口之一,官方文档特意用一节 Notes 强调:
size()并不返回字符串的字符(或字节)长度。
当一个 JSON 值存放了字符串"Hello, world"时,size()返回1,因为该 JSON 值本身是“一个字符串元素”,而不是字符串包含的 12 个字符。同理,binary类型(用于存放 CBOR、BSON 等二进制数据或 subtype 字节序列,见 binary 相关文档)在早期版本中曾被当作容器处理,但从 3.8.0 起也统一为返回1——它同样被视为一个整体“二进制值”。
那么想拿到字符串真实长度该怎么办?正确做法是先把字符串取出来,再调用底层string_t(默认即std::string)的size(),例如:
// 拿到字符串的长度(字节数) std::size_t len = j_string.get_ref<const json::string_t&>().size(); // 或借助 get<std::string> 后再调用 std::size_t len2 = j_string.get<std::string>().size();若你的目标是“判断 JSON 值是否为空”(对对象/数组指 0 个键值对/元素,对 null 指空),更推荐使用empty()而不是size() == 0,二者在标量上的语义相同,但empty()在语义表达上更贴合意图。
完整可运行示例与输出解读
官方文档通过 size.cpp 展示了覆盖全部值类型的调用方式,示例对九种 JSON 值逐一调用size():
#include <iostream> #include <nlohmann/json.hpp> using json = nlohmann::json; int main() { // create JSON values json j_null; json j_boolean = true; json j_number_integer = 17; json j_number_float = 23.42; json j_object = {{"one", 1}, {"two", 2}}; json j_object_empty(json::value_t::object); json j_array = {1, 2, 4, 8, 16}; json j_array_empty(json::value_t::array); json j_string = "Hello, world"; // call size() std::cout << j_null.size() << '\n'; std::cout << j_boolean.size() << '\n'; std::cout << j_number_integer.size() << '\n'; std::cout << j_number_float.size() << '\n'; std::cout << j_object.size() << '\n'; std::cout << j_object_empty.size() << '\n'; std::cout << j_array.size() << '\n'; std::cout << j_array_empty.size() << '\n'; std::cout << j_string.size() << '\n'; }运行输出如下(与 size.output 完全一致):
0 1 1 1 2 0 5 0 1逐行解读这份输出可以帮你建立直觉:
| 输入值 | 输出 | 原因 |
|---|---|---|
json j_null;(null) | 0 | null 视为空值 |
true(boolean) | 1 | 标量恒为 1 |
17(number_integer) | 1 | 标量恒为 1 |
23.42(number_float) | 1 | 标量恒为 1 |
{"one":1,"two":2}(object) | 2 | 键值对个数 |
空 object(value_t::object构造) | 0 | 无键值对 |
[1,2,4,8,16](array) | 5 | 元素个数 |
空 array(value_t::array构造) | 0 | 无元素 |
"Hello, world"(string) | 1 | 标量恒为 1,非字符数 |
注意示例中使用json::value_t::object/json::value_t::array显式构造“空容器型值”,这是区分“null 空值”与“空数组/空对象”的常用手法:三者通过size()都得到0,但它们的 JSON 类型语义截然不同,序列化结果分别为null、{}、[]。
该示例是 header-only 的,只需包含 single_include/nlohmann/json.hpp(或include/nlohmann/json.hpp),以 C++11 及以上标准编译即可运行,无需额外链接库。
复杂度与异常安全保证
官方文档明确给出两条契约:
- 复杂度:常量时间
O(1),前提是array_t与object_t满足 C++ 标准中的Container概念——即它们自身的size()为常量复杂度。默认的std::vector与基于比较器的键控容器(std::map语义)都满足该要求,因此在默认配置下可以放心地把size()用在循环条件、内存预分配计算等高频路径中。 - 异常安全:
noexcept强保证(no-throw guarantee),函数自身永不抛异常。由于实现以const成员访问且对象与数组分支只是调用底层容器的size(),而该调用对默认容器也不抛异常,因此整个调用是安全的。
需要补充一个模板相关的推论(源自源码结构):由于size()被声明为noexcept,若用户自定义的object_t/array_t的size()声明为可能抛出异常,调用时将在noexcept边界触发std::terminate风险;默认容器类型不存在此问题。因此若在自定义容器场景下使用本接口,应确保底层size()同样为无抛出实现。
与迭代器的一致性:测试如何验证语义
在仓库单元测试中,size()不仅验证“数值正确”,还验证了它与迭代器的一致性契约——对一个容器的前向/反向迭代区间做std::distance的结果必须与size()相等。见 tests/src/unit-capacity.cpp 中的SECTION("size()")用例,例如对 boolean 值:
json j = true; const json j_const = true; CHECK(j.size() == 1); CHECK(j_const.size() == 1); // definition of size CHECK(std::distance(j.begin(), j.end()) == j.size()); CHECK(std::distance(j_const.begin(), j_const.end()) == j_const.size()); CHECK(std::distance(j.rbegin(), j.rend()) == j.size()); CHECK(std::distance(j_const.crbegin(), j_const.crend()) == j_const.size());测试同时覆盖 boolean、string、array、object 等类型,以及const版本的对象,验证了:
- 只读语义:
const对象同样可以安全调用size(); - 标量可迭代:即使 boolean/string 这样的标量,也提供 begin/end 迭代器(表现为单个元素),因此
size()为 1 与迭代区间长度为 1 严格一致; - 反向迭代器一致:
rbegin()/rend()、crbegin()/crend()的区间长度同样等于size()。
这一组测试表明,size()是库对“JSON 值可迭代视图”的统一入口度量:无论底层是数组、对象还是单个标量,迭代遍历到的元素总数恰好等于size()。实践中这意味着你可以放心地用size()预估遍历代价、配合下标访问或at()检查边界。
版本演进历史
size()自1.0.0起加入(该接口与库同源,一直是 basic_json 的基础成员);- 在3.8.0中扩展,使
binary类型(字节容器 + 可选 subtype)也返回1。在此之前二进制类型未被纳入统一的标量计数规则,这一变更使 CBOR/BSON 等二进制值在容量语义上与 string 对齐,避免了“二进制按字节数还是按单元数计数”的歧义。
小结:何时该用 size(),何时不该
| 需求场景 | 推荐做法 |
|---|---|
| 判断对象键值对个数 / 数组元素个数 | j.size() |
| 判断 JSON 值是否为 null | j.is_null()(或j.type() == json::value_t::null) |
| 判断对象/数组是否为空 | j.empty(),等价于j.is_null() || j.size() == 0的可读替代 |
| 获取字符串内字符/字节长度 | 先get_ref<const json::string_t&>()再取.size(),而非直接j.size() |
| 获取二进制负载字节数 | 通过j.get_binary()访问bytes.size(),而非直接j.size() |
| 遍历容器时预估迭代总数 | j.size()(与 begin/end 区间长度严格一致,见测试证据) |
一句话总结:size()回答的是“这个 JSON 值有几个顶层元素”,而不是“里面存了多少字符或字节”。牢牢把握“对象/数组看内部、其余标量恒为 1、null 恒为 0”这一规则,就能在任何涉及 JSON 容量判断与遍历度量的代码中游刃有余。更深入的实现细节可继续阅读 basic_json 类的空判断实现、数组类型定制 与 对象类型定制 等相关 API 文档,以及头文件中size()、empty()、max_size()三者的并列实现。
【免费下载链接】jsonJSON for Modern C++项目地址: https://gitcode.com/GitHub_Trending/js/json
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考