Blog 2026 08 05 C++26: #embed
Post
Cancel

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++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.

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 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.

The syntax

#embed is a preprocessor directive. At its simplest:

1
2
3
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.

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:

1
2
#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.

limit

Restricts how many elements are produced:

1
2
3
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.

prefix and suffix

Prepend or append token sequences — but only when the resource is non-empty:

1
2
3
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.

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.

if_empty

Provides fallback content when the resource exists but has zero bytes:

1
2
3
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.

__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.

__has_embed lets you check whether a resource exists and whether it has content — before trying to embed it:

1
2
3
4
5
6
7
8
#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:

MacroValueMeaning
__STDC_EMBED_NOT_FOUND__0Resource not found
__STDC_EMBED_FOUND__1Found, non-empty
__STDC_EMBED_EMPTY__2Found, but empty

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.

__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.

A realistic example

Here’s a pattern I like: embedding a default configuration that the program can use if no external config file is found at runtime.

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";
}

No build system plumbing, no code generation step, no out-of-sync risk. The default configuration is baked into the binary at compile time, available as a constexpr array.

Compiler support and compile times

GCC 15 has full support (-std=c++26). Clang 19+ supports #embed but treats it as a C23 extension in C++ mode — add -Wno-c23-extensions to suppress the warning. MSVC does not support it yet. On Compiler Explorer, you can try it with GCC 15+ or look for “x86-64 clang (thephd.dev)” in the compiler list — JeanHeyd Meneide’s custom Clang build has had #embed support for years.

A natural concern with large files: does the compiler have to pretend it’s parsing ten million integer literals? In principle, yes — #embed is defined as expanding to a comma-separated list. But in practice, both GCC and Clang fast-path it internally. According to the benchmarks shared in P1967R14, GCC embeds a 100 MB file in about 1.3 seconds using 117 MiB of RAM; the equivalent xxd-generated header takes 139 seconds and 13 GiB.

Conclusion

#embed is one of those features that makes you wonder why it took so long. Embedding binary resources into a C or C++ program was a solved problem for decades — just not solved in the language. Every project had its own incantation of xxd, objcopy, or custom scripts, each with its own portability and staleness issues.

With #embed, the resource is part of the source. The compiler reads it, the build system doesn’t need to know about it, and there’s no generated file to keep in sync. The parameters — limit, prefix, suffix, if_empty — handle the framing and fallback patterns that always required wrapper scripts before. It’s a small addition syntactically, but it removes an entire category of build system complexity.

Connect deeper

If you liked this article, please

This post is licensed under CC BY 4.0 by the author.

For C++ developers who give a damn

Better code. Better career. One email a week on modern C++ and what it takes to grow — written by someone still figuring it out too.

    We won't send you spam. Unsubscribe at any time.