使用C++26静态反射在编译时解析JSON
摘要
C++26的#embed和静态反射,结合simdjson库,允许在编译时解析JSON,将配置文件转化为编译时常量,无运行时开销。
<p><a href="https://lobste.rs/s/xqxidz/parsing_json_at_compile_time_with_c_26">评论</a></p>
查看缓存全文
缓存时间: 2026/06/15 09:05
# 在编译时使用 C++26 静态反射解析 JSON
来源:https://lemire.me/blog/2026/06/14/parsing-json-at-compile-time-with-c26-static-reflection/
假设你有一个 JSON 格式的配置文件。看起来像这样:
```json
{ "width": 1920, "height": 1080, "fullscreen": true, "title": "My Game", "volume": 0.8 }
```
通常你会把这个文件连同程序一起发布,启动时打开它、读取并解析。对于永不变化的数据来说,这工作量大得多。如果该文件在构建时就已固定呢?编译器能否读取、解析它,并将结果直接作为常量嵌入到可执行文件中?
C++26 给出了肯定的答案。我们需要两个新特性,所有这些特性现在都可以在最新的 GCC 编译器(版本 16)中使用:
1. `#embed` 指令,用于在编译时将文件拉入程序;
2. 一个支持 *静态反射* 的软件库,例如 simdjson。
让我展示一下我们能走多远。新的 `#embed` 指令读取一个文件并将其展开成一个逗号分隔的字节值列表。要在编译时读取文件 `data.json` 并作为常量保留,我们可以这样写:
```cpp
constexpr const char json_data[] = {
#embed "data.json"
, 0
};
```
我使用了 `constexpr`,因为我希望编译器能够在常量求值过程中检查这些字节。尾部的 `, 0` 只是附加了一个空终止符,这样该数组就可以当作普通的 C 字符串来使用。完全没有运行时 I/O。这些字节就是程序的一部分。
但是嵌入的字节本身还没有用处。我真正想要的是一个类型化的 C++ 对象。在我的示例中,目标类型是这个配置结构体:
```cpp
struct Window {
int width;
int height;
bool fullscreen;
std::string title;
double volume;
};
```
传统上,从 JSON 填充这样的结构体需要为每个字段手工编写一行代码:读取 `"width"`,存储到 `width` 中;读取 `"height"`,存储到 `height` 中,以此类推。这很繁琐。而且因为它在启动时执行,一个格式错误的文件会变成运行时错误,由你的用户(而不是你)发现。
最新版本的 [simdjson](https://github.com/simdjson/simdjson) 可以使用 C++26 静态反射在 *编译时* 解析 JSON。入口点是 `simdjson::compile_time::parse_json`,它做的事情让我觉得有点神奇:它读取 JSON,并根据找到的键为你合成结构体类型。
```cpp
#define SIMDJSON_STATIC_REFLECTION 1
#include "simdjson.h"
constexpr const char json_data[] = {
#embed "data.json"
, 0
};
constexpr auto window = simdjson::compile_time::parse_json<Window>();
```
变量 `window` 是一个完全由编译器计算出来的值。它的类型是从文档生成的:它有一个 `width` 和一个 `height`(都是 64 位整数),一个 `bool` 类型的 `fullscreen`,一个 `double` 类型的 `volume`,以及一个 `title`。此后我就可以写 `window.width`,它表现得就像任何普通字段一样。
我怎么知道解析真正发生在编译时?因为我可以对结果进行断言,这些断言必须在程序存在之前由编译器检查:
```cpp
static_assert(window.width == 1920);
static_assert(window.height == 1080);
static_assert(window.fullscreen == true);
```
如果我损坏了 JSON——删除一个花括号、拼错 `true`、留下一个尾随逗号——程序将无法编译,错误指向 `parse_json` 那一行。损坏的文件在构建时就被发现了,在我自己的机器上,而不是在别人机器上的启动时。
由于 `window` 是真正的编译时常量,任何基于它的计算也都是常量。考虑这个函数:
```cpp
int screen_area() {
return window.width * window.height;
}
```
用 `-O3` 编译后,没有乘法,没有字段访问,当然也没有任何解析残留——只剩下答案作为立即数(这里是在我的 MacBook 上):
```asm
screen_area:
mov w0, #0xa400 // 0x1fa400 = 2073600
movk w0, #0x1f, lsl #16
ret
```
JSON 已经从二进制中消失了。它被编译器读取并解析了恰好一次,唯一存活下来的就是数字 2073600。
由于静态反射非常新,在使用 GCC 16 构建时,你需要传递标志 `-std=c++26 -freflection`:`-freflection` 标志是激活编译时反射所必需的。你还必须在导入 `simdjson.h` 之前设置 simdjson 宏 `SIMDJSON_STATIC_REFLECTION=1`。这是一个临时安全措施。
重现这些示例的源代码可以在 [这里](https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog/tree/master/2026/06/16) 找到。
*参考*:
[P2996 — Reflection for C++26](https://wg21.link/p2996) 和 [simdjson 库](https://simdjson.org/)。
*鸣谢*:simdjson 的实现是与 Francisco Geiman Thiesen 共同完成的。
相似文章
@shubh6200: 要了解如何处理海量文件,请阅读 @geofflangdale 和 @lemir 的《Parsing Gigabytes of JSON per Second》…
该论文介绍了 simdjson,这是第一个能够在单核上使用 SIMD 指令每秒处理数 GB 数据的验证性 JSON 解析器,相比 RapidJSON 等现有解析器实现了显著的加速。
C++26: #embed
C++26 introduces the #embed preprocessor directive, allowing binary files to be embedded directly at compile time without external tools, simplifying resource inclusion in C++ programs.
使用计算着色器在GPU上进行并行JSON解析
slurpjson是一个Rust库,它完全在GPU上使用wgpu计算着色器解析JSON,将解析过程分解为并行前缀扫描,用于研究目的。
枚举转字符串的开销:C++26 反射与旧方法对比
本文使用 GCC 16 基准测试了 C++26 反射在枚举转字符串转换中的编译时开销,并将其与 C++17 库和 X 宏预处理器技术进行了对比。
JSON5E - 人类友好的JSON5
libpdjson5 是一个公共领域的 C 语言库,用于解析 JSON、JSON5 和 JSON5E,具有完整的 Unicode 支持、极小的内存占用和流式 API。它是 pdjson 的一个分支,进行了多项改进,包括对 JSON5E 的支持。