C++26: #embed

C++26: #embed

If you’ve ever needed to ship a binary file — a certificate, a small image, a default configuration — inside a C++ program, you know the ritual. You find or write a tool that converts the file into a C array, you wire it into your build system, and you pray that nobody forgets to re-run the conversion after updating the original file.

如果你曾经需要在 C++ 程序中内置二进制文件(如证书、小图标或默认配置文件),你一定很熟悉这套流程:你需要寻找或编写一个工具将文件转换为 C 数组,将其集成到构建系统中,并祈祷在更新原始文件后,没有人忘记重新运行转换程序。

C++26 ends this with #embed (P1967R14 by JeanHeyd Meneide). Think of it as #include for binary data — a preprocessor directive that turns a file into a comma-separated sequence of integer constant expressions, directly at compile time, with no external tools.

C++26 通过 #embed(由 JeanHeyd Meneide 提出的 P1967R14)终结了这一繁琐过程。你可以把它看作是二进制数据的 #include —— 它是一个预处理指令,能在编译时直接将文件转换为逗号分隔的整数常量表达式序列,无需任何外部工具。

Before #embed, every project rolled its own approach — xxd -i, objcopy, linker tricks, Python scripts, CMake file(READ) — each fragile, platform-specific, and one forgotten regeneration away from being stale.

在 #embed 出现之前,每个项目都有一套自己的方案——使用 xxd -i、objcopy、链接器技巧、Python 脚本或 CMake 的 file(READ) 等。这些方法往往脆弱且依赖特定平台,一旦忘记重新生成,数据就会变得陈旧。

#embed is also a perfect example of how long the standardization process can take. The first revision of P1967 was submitted in 2020, and it took fourteen revisions over five years before the committee voted it in. Along the way the syntax changed substantially as the design searched for consensus. And P1967 itself was a restart — earlier proposals like P1040 (std::embed) pushed for a non-preprocessor approach, embedding resources through a constexpr function rather than a directive. That path didn’t find enough support, and JeanHeyd eventually pivoted to the preprocessor-based design that made it through.

#embed 也是标准化过程漫长的一个典型例子。P1967 的第一个版本于 2020 年提交,历经五年、十四次修订,才最终获得委员会投票通过。在此过程中,为了达成共识,语法经历了重大调整。P1967 本身也是一次“重启”——早期的提案(如 P1040 std::embed)曾尝试非预处理方案,即通过 constexpr 函数而非指令来嵌入资源。由于该路径支持度不足,JeanHeyd 最终转向了现在通过的基于预处理器的设计。

The syntax

语法

#embed is a preprocessor directive. At its simplest: #embed 是一个预处理指令。最简单的用法如下:

const unsigned char icon[] = { #embed "icon.png" };

The directive reads icon.png and expands to a comma-separated list of integer constant expressions, one per byte. Each value is in the range [0, 255] (assuming CHAR_BIT == 8, which it is on every platform you care about). The result is exactly what xxd -i would have produced — but without the extra tool, the build step, or the generated file.

该指令会读取 icon.png,并将其展开为逗号分隔的整数常量表达式列表,每个字节对应一个值。每个值都在 [0, 255] 范围内(假设 CHAR_BIT == 8,这在所有主流平台上都成立)。其结果与 xxd -i 生成的内容完全一致,但省去了额外的工具、构建步骤和生成的中间文件。

The resource identifier follows the same rules as #include: double quotes search implementation-defined paths (typically starting with the source file’s directory), and angle brackets search the system include paths: 资源标识符遵循与 #include 相同的规则:双引号搜索实现定义的路径(通常从源文件所在目录开始),尖括号则搜索系统包含路径:

#embed <default_config.json> // system resource path
#embed "local_asset.bin"      // local path first

Embed parameters

嵌入参数

What makes #embed more than a built-in xxd are its four standard parameters. They are specified in parentheses after the resource identifier, using a syntax borrowed from attributes. #embed 之所以不仅仅是一个内置的 xxd,是因为它拥有四个标准参数。这些参数在资源标识符后的括号中指定,语法借鉴了属性(attributes)。

limit Restricts how many elements are produced: 限制生成的元素数量:

const unsigned char header[] = { #embed "firmware.bin" limit(64) };

This embeds only the first 64 bytes. Useful for pulling in just a file header, a magic number, or a fixed-size prefix without embedding the entire resource. 这只会嵌入前 64 个字节。适用于仅提取文件头、魔数或固定大小的前缀,而无需嵌入整个资源。

prefix and suffix Prepend or append token sequences — but only when the resource is non-empty: 在资源非空时,在序列前后添加标记序列:

const unsigned char data[] = { #embed "payload.bin" prefix(0xAA, 0xBB,) suffix(, 0xCC, 0xDD) };

If payload.bin contains bytes {0x01, 0x02}, this expands to {0xAA, 0xBB, 0x01, 0x02, 0xCC, 0xDD}. If the file is empty, the prefix and suffix are silently omitted — you get an empty initializer, not a stray comma. 如果 payload.bin 包含字节 {0x01, 0x02},则展开为 {0xAA, 0xBB, 0x01, 0x02, 0xCC, 0xDD}。如果文件为空,前缀和后缀会被静默忽略——你得到的是一个空的初始化列表,而不是多余的逗号。

Note the trailing comma in prefix(0xAA, 0xBB,) and the leading comma in suffix(, 0xCC, 0xDD). These aren’t typos — they’re necessary because #embed expands to a token sequence that sits between the prefix and suffix. Without the trailing comma in the prefix, the last prefix token and the first embedded byte would be concatenated incorrectly. 注意 prefix(0xAA, 0xBB,) 中的末尾逗号和 suffix(, 0xCC, 0xDD) 中的起始逗号。这不是笔误,而是必须的,因为 #embed 展开后的标记序列位于前缀和后缀之间。如果没有前缀中的末尾逗号,前缀的最后一个标记和第一个嵌入字节就会错误地连接在一起。

if_empty Provides fallback content when the resource exists but has zero bytes: 当资源存在但字节数为零时,提供回退内容:

const unsigned char config[] = { #embed "user_overrides.cfg" if_empty('{', '}') };

If user_overrides.cfg is empty, you get {’{’, ’}’} — a minimal valid JSON object as raw bytes. If the file has content, if_empty is ignored. Note that when if_empty applies, prefix and suffix are also suppressed — you get exactly the if_empty tokens and nothing else. 如果 user_overrides.cfg 为空,你将得到 {’{’, ’}’} —— 一个作为原始字节的最小有效 JSON 对象。如果文件有内容,if_empty 将被忽略。注意,当 if_empty 生效时,前缀和后缀也会被抑制——你只会得到 if_empty 中的标记,不会有其他内容。

__has_embed

You might not have seen this pattern before, but #include actually has a companion preprocessor test too — __has_include, available since C++17. Most of us never needed it because we control our own includes. #embed gets the same treatment with __has_embed, and here it’s more likely to be useful: the resource you want to embed might genuinely not exist in all build environments. 你可能没见过这种模式,但 #include 其实也有一个配套的预处理测试 —— __has_include(自 C++17 起可用)。我们大多数人从未用到它,因为我们能控制自己的包含文件。#embed 也通过 __has_embed 获得了同样的待遇,而且在这里它更有用:你想要嵌入的资源可能确实不会存在于所有构建环境中。

__has_embed lets you check whether a resource exists and whether it has content — before trying to embed it: __has_embed 允许你在尝试嵌入资源之前,检查该资源是否存在以及是否有内容:

#if __has_embed("branding.png")
    const unsigned char branding[] = { #embed "branding.png" };
#else
    // fall back to a compiled-in default
    const unsigned char branding[] = { /* ... */ };
#endif

__has_embed returns one of three values: __has_embed 返回以下三个值之一:

MacroValueMeaning
STDC_EMBED_NOT_FOUND0Resource not found
STDC_EMBED_FOUND1Found, non-empty
STDC_EMBED_EMPTY2Found, but empty
含义
STDC_EMBED_NOT_FOUND0未找到资源
STDC_EMBED_FOUND1已找到,非空
STDC_EMBED_EMPTY2已找到,但为空

Since STDC_EMBED_NOT_FOUND is 0 and the other two are truthy, a plain #if __has_embed(…) covers the common case of “embed if available.” If you need to distinguish between found-empty and found-with-content, compare against the specific macros. 由于 STDC_EMBED_NOT_FOUND 为 0,而另外两个值为真,因此简单的 #if __has_embed(…) 即可覆盖“如果可用则嵌入”的常见场景。如果你需要区分“已找到但为空”和“已找到且有内容”,则需与特定的宏进行比较。

__has_embed also accepts the same parameters as #embed. This matters because some parameters can affect whether the result is considered “empty.” For example, __has_embed(“data.bin” limit(0)) returns STDC_EMBED_EMPTY regardless of the file’s actual size — you asked for zero bytes. __has_embed 也接受与 #embed 相同的参数。这一点很重要,因为某些参数会影响结果是否被视为“空”。例如,__has_embed(“data.bin” limit(0)) 无论文件实际大小如何,都会返回 STDC_EMBED_EMPTY —— 因为你要求的是零字节。