news 2026/9/9 22:57:16

JSON for Modern C++:全面掌握在内存中创建 JSON 值的五种核心方式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSON for Modern C++:全面掌握在内存中创建 JSON 值的五种核心方式

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::vectorstd::dequestd::liststd::forward_liststd::arraystd::valarraystd::setstd::unordered_setstd::multisetstd::unordered_multiset等任意元素可转 JSON 的容器;
  • 对象(object)object_t以及std::mapstd::unordered_mapstd::multimapstd::unordered_multimap等键类型兼容string_t、值类型可转 JSON 的关联容器;
  • 字符串(string)string_t、字符串字面量及兼容的字符串容器;
  • 数值(number)number_integer_tnumber_unsigned_tnumber_float_t以及所有可转换的数值类型(intsize_tint64_tfloatdouble等);
  • 布尔(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 的说明,库采用的判定规则为:

  1. 若列表为空,构造空的 JSON对象{}——因为 C++ 的空花括号{}语义上最接近空对象;
  2. 若列表元素全是"以字符串开头的二元素子列表"(即 key/value 对),则构造对象,每对的第一元素为键、第二元素为值;
  3. 其余一切情况,构造数组

这样设计的理由: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置为falsemanual_type指定为value_t::arrayvalue_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(关闭,保持既有行为)。

如果你希望花括号初始化的对象/数组内容符合直觉,有三种处理方式:

  1. 显式创建单元素数组:json j = json::array({obj});,这样无论何时都得到[obj]
  2. 拷贝用圆括号:json j3(j1);
  3. 选择加入宏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以及inserterase等修改函数继续拼装,详见 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 类型初始值
nullnull
booleanfalse
string""
number0
object{}
array[]
binary空数组

该构造函数的后置状态可通过clear()恢复。

批量副本——basic_json(size_type cnt, const basic_json& val)生成含cntval副本的数组;cnt为 0 时得到空数组。

迭代器区间构造——basic_json(iterator first, iterator last)[first, last)的内容构造:对数组/对象类型,语义类似std::vector/std::map的区间构造;对基本类型,要求first恰为begin()lastend()(否则抛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_backemplaceinserterase等后续修改手段
  • Parsing:从 JSON 文本、流或迭代器区间解析得到值

【免费下载链接】jsonJSON for Modern C++项目地址: https://gitcode.com/GitHub_Trending/js/json

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 22:56:03

Docker Swarm服务生命周期管理实战:从部署到故障转移

接手生产环境之后你会发现&#xff0c;用 Docker Swarm 管理集群&#xff0c;真正的难点从来不是“搭起来”&#xff0c;而是把服务的整个生命周期管明白。这篇文章以 Docker 29.1.3 环境为基础&#xff0c;围绕 Swarm 集群的服务生命周期管理展开&#xff0c;从集群初始化、节…

作者头像 李华
网站建设 2026/9/9 22:52:01

VMware存储卷扩展实战:从底层原理到VMFS在线扩容全解析

1. 存储卷的底层逻辑&#xff1a;先搞清楚“卷”到底是什么讲存储卷之前&#xff0c;先想一个问题&#xff1a;你在虚拟机里看到的C盘、D盘&#xff0c;和你在ESXi主机上看到的“datastore1”到底差了多少层&#xff1f;答案是&#xff1a;差了很多层&#xff0c;但很多运维干了…

作者头像 李华
网站建设 2026/9/9 22:48:02

Spring @Async异步任务深度解析:线程池配置与常见坑

Spring 里做异步任务&#xff0c;很多人第一反应就是 Async。这个注解确实省事&#xff0c;一个注解扔上去&#xff0c;方法调用就自动丢进线程池跑&#xff0c;看起来人畜无害。但我在实际项目里见过太多人在这上面栽跟头&#xff0c;有的是方法内部调用不生效&#xff0c;有的…

作者头像 李华
网站建设 2026/9/9 22:47:57

VxWorks串口通信实战:从termios配置到Zynq平台部署

简介&#xff1a;VxWorks广泛部署于航空航天、通信、工业自动化等实时性要求极高的场景&#xff0c;串口通信则是设备交互与调试环节中最常用也最基础的手段之一。这份示例程序以TestUart项目为载体&#xff0c;面向嵌入式初学者与工程开发人员&#xff0c;重点演示VxWorks标准…

作者头像 李华