JSON for Modern C++:全面掌握在内存中创建 JSON 值的五种核心方式
【免费下载链接】jsonJSON for Modern C++项目地址: https://gitcode.com/GitHub_Trending/js/json
本文基于 nlohmann/json(JSON for Modern C++)官方文档 creating_values.md 编写,系统讲解在内存中构建json对象的完整方法:从原生 C++ 值直接转换、用花括号初始化列表简洁书写、通过operator[]增量搭建嵌套结构,以及利用_json用户自定义字面量原位解析 JSON 文本。读完本文,你将理解这些方式各自的适用场景、类型推断规则与易错歧义点(空对象 vs 空数组、单元素花括号初始化),并能在实际工程中写出可读、准确、无歧义的 JSON 构建代码。
JSON 值有两种来源:要么从 JSON 文本解析而来,要么直接在内存中创建。本文聚焦后者——如何把 C++ 世界的数值、字符串、容器转换成json;若想从文本或流解析,参见 Parsing 相关章节。
一、从 C++ 值直接构造:一条赋值语句生成一个 JSON
任何一个受支持的 C++ 类型,都可以直接赋值给json,或作为构造参数传入。这是最直观的创建方式:
json j_number = 42; // number(整数) json j_float = 3.141; // number(浮点) json j_string = "Hello"; // string json j_boolean = true; // boolean json j_null = nullptr; // null json j_vector = std::vector<int>{1, 2, 3}; // array: [1, 2, 3]这段示例来自 creating_values.md 的 "From C++ values" 小节。其底层依赖basic_json的"万能兼容构造函数":
template<typename CompatibleType> basic_json(CompatibleType&& val) noexcept(...);按 basic_json 构造函数文档 的说明,该重载把所有定义了to_json()的类型"接住",并把参数val转发给对应的json_serializer<U>::to_json(其中U = uncvref_t<CompatibleType>)。它支持的类型非常宽泛:
- 数组(array):
array_t以及std::vector、std::deque、std::list、std::forward_list、std::array、std::valarray、std::set、std::unordered_set、std::multiset、std::unordered_multiset等任意元素可转 JSON 的容器; - 对象(object):
object_t以及std::map、std::unordered_map、std::multimap、std::unordered_multimap等键类型兼容string_t、值类型可转 JSON 的关联容器; - 字符串(string):
string_t、字符串字面量及兼容的字符串容器; - 数值(number):
number_integer_t、number_unsigned_t、number_float_t以及所有可转换的数值类型(int、size_t、int64_t、float、double等); - 布尔(boolean):
boolean_t/bool; - 二进制(binary):
binary_t/std::vector<uint8_t>。注意:由于 C++ 类型系统无法区分字符串字面量与二进制字符数组,所有兼容const char*的类型都会被导向字符串构造函数——这是出于向后兼容的既有设计。
C++ 各标量类型与 JSON 类型的对应关系总结见 conversions,其中列出了可参与转换的完整类型清单与to_json/from_json定制机制。
二、用花括号初始化列表:一眼读懂数组与对象的写法
当需要一次性写出结构化的对象或数组时,最优雅的方式是花括号初始化列表:
// 一个数组 json array = {1, 2, 3, 4}; // 一个对象(由若干 key/value 对组成) json object = { {"pi", 3.141}, {"happy", true}, {"name", "Niels"}, {"nothing", nullptr}, {"list", {1, 0, 2}}, {"object", {{"currency", "USD"}, {"value", 42.99}}} };外层括号内的每个元素既可以嵌套数组、对象,也可以混合不同类型的值。仓库自带的示例 basic_json__list_init_t.cpp 完整展示了嵌套场景的实际输出(对应 output):
json j_empty_init_list = json({}); // {} json j_object = { {"one", 1}, {"two", 2} }; // {"one":1,"two":2} json j_array = {1, 2, 3, 4}; // [1,2,3,4] json j_nested_object = { {"one", {1}}, {"two", {1, 2}} }; // {"one":[1],"two":[1,2]} json j_nested_array = { {{1}, "one"}, {{1, 2}, "two"} }; // [[[1],"one"],[[1,2],"two"]]底层类型推断规则
关键在于:数组还是对象,由列表内容动态决定。按 basic_json 构造函数文档(重载 5)与 creating_values.md 的说明,库采用的判定规则为:
- 若列表为空,构造空的 JSON对象
{}——因为 C++ 的空花括号{}语义上最接近空对象; - 若列表元素全是"以字符串开头的二元素子列表"(即 key/value 对),则构造对象,每对的第一元素为键、第二元素为值;
- 其余一切情况,构造数组。
这样设计的理由:C++ 没有描述映射类型的原生活法,只能以"二元组列表"表示映射;而 JSON 规定键必须是字符串,因此规则 2 是判定对象最宽松的约束;其余情况按数组解释是安全的兜底。规则 1 的代价是无法用空初始化列表表达空数组。
三、歧义边界与显式工厂函数json::array/json::object
正因为{}语法同时承担数组与对象两种身份,某些场景会产生歧义。官方文档特别给出警告,并建议使用显式工厂函数 json::array 与 json::object 强制指定目标类型:
json empty_array_explicit = json::array(); // [] json empty_object_explicit = json::object(); // {} // 想要"只含一个对象的数组",而不是"含一个键值对的对象" json array_of_objects = json::array({{"key", "value"}}); // [{"key":"value"}]json::array(initializer_list_t init = {})
该静态函数把传入的初始化列表原样包装为数组;省略参数或传空列表即得空数组[]。仓库示例 array.cpp 覆盖了四种关键形态:
json j_no_init_list = json::array(); // [] json j_empty_init_list = json::array({}); // [] json j_nonempty = json::array({1, 2, 3, 4}); // [1,2,3,4] json j_list_of_pairs = json::array({ {"one", 1}, {"two", 2} }); // [{"one":1},{"two":2}]值得注意最后一行:json::array接收的是"元素为键值对"的列表,结果却是数组(数组内每个元素才是对象),这正是array()存在的核心价值——同样的初始化列表若直接交给普通花括号构造,会被推断成对象。
json::object(initializer_list_t init = {})
该函数强制按对象语义解析列表:元素必须是二元组,且每个二元组的首元素必须是字符串,否则抛出type_error.301。按 object 文档 的说法,object()主要是为对称性而存在——普通初始化列表构造已经能表达任何对象;真正不可替代的只有array()处理的两类边界(空数组、"键值对数组")。
这些工厂函数本质上是把type_deduction置为false、manual_type指定为value_t::array或value_t::object的特化形式,见 basic_json 构造函数 的参数说明。若强制对象但列表无法构成键值对,构造函数会抛出type_error.301;而同一列表若走自动推断,则会退化为数组。
四、最容易踩坑的歧义:单元素花括号初始化
与上面相关还有一个陷阱:json j{value};这种单元素花括号初始化默认会把value包进一个单元素数组,而且这一行为历史上甚至因编译器而异(GCC 会包装,旧版 Clang 不会;自 Clang 20 起两者行为已一致)。官方 FAQ 的 brace-initialization-yields-arrays 条目给出了典型对比:
json j1 = "hello"; json j2{j1}; // j2 是 ["hello"],并不是 j1 的拷贝! json j3(j1); // j3 是 "hello" —— 圆括号才是拷贝原因在 json_brace_init_copy_semantics 宏文档 中有底层解释:C++ 在花括号初始化时总是优先匹配initializer_list构造函数,而不是拷贝/移动构造函数。该库默认值为0(关闭,保持既有行为)。
如果你希望花括号初始化的对象/数组内容符合直觉,有三种处理方式:
- 显式创建单元素数组:
json j = json::array({obj});,这样无论何时都得到[obj]; - 拷贝用圆括号:
json j3(j1); - 选择加入宏
JSON_BRACE_INIT_COPY_SEMANTICS,让单元素花括号初始化退化为拷贝/移动语义。注意该宏必须在#include <nlohmann/json.hpp>之前定义,且在 include 之后定义无效:
#define JSON_BRACE_INIT_COPY_SEMANTICS 1 #include <nlohmann/json.hpp>五、增量构建:用operator[]边访问边创建
当 JSON 结构需要逐层搭建(例如从配置数据逐项填充)时,可以借助operator[]的自动创建特性:访问一个尚不存在的对象键或数组下标时,库会按需在内存中即时创建对应元素(含中间层)。creating_values.md 给出的精炼示例:
json j; // 初始为 null j["answer"]["everything"] = 42; // 自动升级为对象并写入 {"answer":{"everything":42}} j["list"] = {1, 0, 2}; // 添加数组键 j["list"].push_back(3); // 数组尾部追加,变为 [1,0,2,3]第一行json j;默认调用无参/null 构造函数,得到一个 JSONnull值;随后j["answer"]访问不存在的键,operator[]便将其按值类型展开——先让j成为对象,再让j["answer"]成为下一层对象,从而完成"answer"."everything" = 42的深层写入。整个过程可读性极强,构建顺序与 JSON 结构天然一致。
需要扩展元素、在中间位置插入时,可配合 push_back、emplace以及insert、erase等修改函数继续拼装,详见 modifying values。
六、_json字面量:把 JSON 文本写进代码、原位解析
如果你希望代码里直接出现一段"类 JSON 语法",并让它在编译期字符串所在处就被解析成json值,那么用户自定义字面量_json是最合适的选择。仓库文档 operator_literal_json.cpp 展示了一个可直接编译运行的完整示例:
#include <iostream> #include <iomanip> #include <nlohmann/json.hpp> using json = nlohmann::json; using namespace nlohmann::literals; int main() { json j = R"( {"hello": "world", "answer": 42} )"_json; std::cout << std::setw(2) << j << '\n'; }格式化输出结果为(见 operator_literal_json.output):
{ "answer": 42, "hello": "world" }作用域与命名空间
字面量操作符按标准做法放入命名空间,库推荐用以下任一方式引入,以便后续迁移到下一主版本:
using nlohmann::literals::operator ""_json; using namespace nlohmann::literals; using namespace nlohmann::json_literals; using namespace nlohmann::literals::json_literals; using namespace nlohmann;(如需让字面量全局可用,可了解宏JSON_USE_GLOBAL_UDLS。)_json字面量自版本 1.0.0 提供,3.11.0 移入nlohmann::literals::json_literals命名空间,3.13.0 起新增char8_t*重载(C++20)。对应实现与回归测试可见 unit-udl.cpp。
关键区分:解析 vs 字符串
_json的本质是解析,所以它与字符串构造函数的结果截然不同——这是文档明确强调、也最容易混淆的点:
auto a = "42"_json; // number:42 json b = json("42"); // string:"42""42"_json调用的是operator""_json(const char*, size_t),内部等价于对这段文本执行一次parse(s, s+n),因此任何parse会抛出的解析错误(如非法 JSON)它同样会抛出;而json("42")走的是字符串兼容构造函数,生成的是一个值为"42"的 JSON 字符串。
七、更多构造途径:类型化空值、拷贝/移动、迭代器区间与批量副本
creating_values.md在文末将读者引导至 basic_json 构造函数总文档,那里完整列出全部 9 个构造函数重载。除前述内容外,还有几个实用入口值得了解:
按类型创建默认空值——basic_json(value_t v)可指定类型并得到其"空初值":
| 指定的 value 类型 | 初始值 |
|---|---|
| null | null |
| boolean | false |
| string | "" |
| number | 0 |
| object | {} |
| array | [] |
| binary | 空数组 |
该构造函数的后置状态可通过clear()恢复。
批量副本——basic_json(size_type cnt, const basic_json& val)生成含cnt个val副本的数组;cnt为 0 时得到空数组。
迭代器区间构造——basic_json(iterator first, iterator last)以[first, last)的内容构造:对数组/对象类型,语义类似std::vector/std::map的区间构造;对基本类型,要求first恰为begin()、last为end()(否则抛invalid_iterator.204);对null值调用会抛invalid_iterator.206。注意两个迭代器必须来自同一 JSON 值(预条件在 assertions 有运行时断言约束)。
拷贝与移动——拷贝构造函数保证*this == other;移动构造函数"窃取"源资源并把源置为null。两者分别提供强异常安全与不抛异常保证。绝大多数构造函数的重载(拷贝、移动、null、计数构造)都是常数级或线性级复杂度,初始化列表构造函数复杂度线性于列表长度。
八、小结与选型建议
综合全文,在内存中创建 JSON 值时应按场景选择:
| 你的需求 | 推荐方式 |
|---|---|
| 单个标量 / 现有 C++ 容器直接转 JSON | 赋值或构造(从 C++ 值转换) |
| 常量对象/数组字面量 | 花括号初始化列表 |
| 空数组、键值对形态的数组 | json::array(...) |
| 强制某键值对形态按对象解析、明确语义 | json::object(...) |
| 数据结构未知、需逐层动态拼装 | 默认构造 +operator[]增量构建 +push_back/emplace |
| 想在源码中直接内联一段 JSON 文本 | "... "_json字面量 |
| 需要拷贝、移动或区间构造 | 对应构造函数重载 |
需要特别防范两类歧义:空花括号得到对象而非数组(用json::array()解决),以及单元素花括号初始化会包装成数组(用圆括号拷贝或JSON_BRACE_INIT_COPY_SEMANTICS解决)。
延伸阅读
- basic_json 构造函数总览:全部 9 种构造方式的签名、语义、异常与复杂度
- json::array / json::object:强制指定数组/对象类型
- operator""_json:
_json字面量的完整签名与版本历史 - Converting values:可参与转换的完整 C++ 类型清单
- Modifying values:
push_back、emplace、insert、erase等后续修改手段 - Parsing:从 JSON 文本、流或迭代器区间解析得到值
【免费下载链接】jsonJSON for Modern C++项目地址: https://gitcode.com/GitHub_Trending/js/json
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考