C++26 引入 #embed:编译时嵌入二进制文件,告别 xxd 和外部工具
语法
`#embed` 是一条预处理器指令。最简单的用法如下: ```cpp const unsigned char icon[] = { #embed "icon.png" }; ``` 这条指令读取 `icon.png`,展开成一个逗号分隔的整数常量表达式列表,每个字节对应一个值。每个值的范围是 [0, 255](假设 `CHAR_BIT == 8`,你关心的所有平台都是这样)。结果和 `xxd -i` 生成的完全一样——但不需要额外工具、不需要构建步骤、也不需要生成文件。资源标识符的规则与 #include 相同:双引号搜索实现定义的路径(通常从源文件所在目录开始),尖括号搜索系统包含路径:
1
2
#embed <default_config.json> // 系统资源路径
#embed "local_asset.bin" // 优先本地路径
嵌入参数
让 #embed 超越内置版 xxd 的是它的四个标准参数。这些参数放在资源标识符后的括号里,语法借鉴了属性。
limit
限制生成的元素数量:
1
2
3
const unsigned char header[] = {
#embed "firmware.bin" limit(64)
};
这只嵌入前 64 个字节。适合只提取文件头、魔数或固定大小的前缀,而无需嵌入整个资源。
prefix 和 suffix
在资源非空时,在前后添加 token 序列:
1
2
3
const unsigned char data[] = {
#embed "payload.bin" prefix(0xAA, 0xBB,) suffix(, 0xCC, 0xDD)
};
如果 payload.bin 包含字节 {0x01, 0x02},则展开为 {0xAA, 0xBB, 0x01, 0x02, 0xCC, 0xDD}。如果文件为空,则前缀和后缀会被静默忽略——你得到的是一个空初始化器,而不是多余的逗号。
注意 prefix(0xAA, 0xBB,) 中的尾随逗号和 suffix(, 0xCC, 0xDD) 中的前导逗号。这不是笔误——因为 #embed 展开成一个 token 序列,它位于前缀和后缀之间。如果没有前缀中的尾随逗号,最后一个前缀 token 和第一个嵌入字节会错误地拼接在一起。
if_empty
当资源存在但为零字节时提供回退内容:
1
2
3
const unsigned char config[] = {
#embed "user_overrides.cfg" if_empty('{', '}')
};
如果 user_overrides.cfg 为空,你会得到 {'{', '}'}——一个最小的合法 JSON 对象,以原始字节形式存在。如果文件有内容,if_empty 会被忽略。注意,当 if_empty 生效时,prefix 和 suffix 也会被抑制——你只会得到 if_empty 指定的内容,没有其他东西。
__has_embed
你可能之前没见过这种模式,但 #include 其实也有一个配套的预处理器测试——__has_include,从 C++17 开始可用。我们大多数人从未需要过它,因为我们能控制自己的包含文件。#embed 也有类似的 __has_embed,而且它更可能派上用场:你想嵌入的资源在某些构建环境中可能确实不存在。
__has_embed 让你在尝试嵌入之前,先检查资源是否存在以及是否有内容:
1
2
3
4
5
6
7
8
#if __has_embed("branding.png")
const unsigned char branding[] = {
#embed "branding.png"
};
#else
// 回退到编译时内置的默认值
const unsigned char branding[] = { /* ... */ };
#endif
__has_embed 返回三个值之一:
| 宏 | 值 | 含义 |
|---|---|---|
__STDC_EMBED_NOT_FOUND__ | 0 | 资源未找到 |
__STDC_EMBED_FOUND__ | 1 | 找到,非空 |
__STDC_EMBED_EMPTY__ | 2 | 找到,但为空 |
由于 __STDC_EMBED_NOT_FOUND__ 是 0,而另外两个是真值,所以简单的 #if __has_embed(...) 就能覆盖“如果可用就嵌入”的常见情况。如果你需要区分“找到但为空”和“找到且有内容”,可以对比特定的宏。
__has_embed 也接受与 #embed 相同的参数。这一点很重要,因为某些参数会影响结果是否被视为“空”。例如,__has_embed("data.bin" limit(0)) 会返回 __STDC_EMBED_EMPTY__,无论文件实际大小如何——因为你只请求了零字节。
一个实际例子
下面是我喜欢的一种模式:嵌入一个默认配置,当程序在运行时找不到外部配置文件时,可以使用这个配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// https://godbolt.org/z/Wb339WTzj
#include <cstdint>
#include <fstream>
#include <iostream>
#include <iterator>
#include <vector>
constexpr unsigned char default_config[] = {
#embed "defaults.json"
};
std::vector<uint8_t> load_config(const char* path) {
std::ifstream file(path, std::ios::binary);
if (!file) {
return {std::begin(default_config), std::end(default_config)};
}
return {std::istreambuf_iterator<char>(file),
std::istreambuf_iterator<char>()};
}
int main() {
auto config = load_config("config.json");
std::cout << "Loaded " << config.size() << " bytes of configuration\n";
}
无需构建系统脚本,无需代码生成步骤,也没有不同步的风险。默认配置在编译时直接嵌入二进制文件,以 constexpr 数组的形式可用。
编译器支持与编译时间
GCC 15 已完全支持(使用 -std=c++26)。Clang 19+ 支持 #embed,但在 C++ 模式下将其视为 C23 扩展——添加 -Wno-c23-extensions 可屏蔽警告。MSVC 尚未支持。在 Compiler Explorer 上,你可以用 GCC 15+ 尝试,或在编译器列表中寻找 "x86-64 clang (thephd.dev)"——JeanHeyd Meneide 的自定义 Clang 构建已支持 #embed 多年。
处理大文件时自然会担心:编译器是否要假装解析一千万个整数字面量?原则上是的——#embed 被定义为展开成逗号分隔的列表。但实际中,GCC 和 Clang 都在内部做了快速路径处理。根据 P1967R14 分享的基准测试,GCC 嵌入一个 100 MB 的文件大约需要 1.3 秒,占用 117 MiB 内存;而等效的 xxd 生成的头文件则需要 139 秒和 13 GiB 内存。
总结
#embed 是那种让你纳闷为什么花了这么长时间才实现的功能。将二进制资源嵌入 C 或 C++ 程序几十年来一直是个已解决的问题——只是没有在语言层面解决。每个项目都有自己的 xxd、objcopy 或自定义脚本的变通方案,各自都存在可移植性和过时风险的问题。
使用 #embed 后,资源直接成为源码的一部分。编译器负责读取,构建系统无需感知,也不再有需要同步的生成文件。limit、prefix、suffix、if_empty 这些参数处理了以往必须借助包装脚本才能实现的框架和回退模式。从语法上看这只是个小改动,但它消除了构建系统中一整类复杂性。
深入交流
如果你喜欢这篇文章,请
- 点赞,
- 订阅我的通讯
- 并在 Twitter 上与我联系!
- 如果你正在准备 C++ 量化/交易面试,请查看 GetCracked