C++26 #embed and Reflection: When the Compiler Starts Doing More Work for You
- 12 minutes ago
- 5 min read
Hi C++ friends 👋
Today we are going to explore another feature from the future of C++.
Or maybe not exactly the future anymore.
C++26 is getting closer, compilers are slowly catching up, and some of the things we used to describe as “maybe one day” are starting to become real tools we can experiment with today.
One of those tools is #embed.
And when we combine it with C++ reflection, things become very interesting.
Not “interesting” like undefined behavior interesting.
Hopefully.
More like: “wait, the compiler can do that now?” interesting.
What is #embed?
Most C++ developers already know #include.
When we write:
#include "user.schema"the preprocessor takes the content of the file and pastes it into the source code.
It is textual inclusion.
Useful, powerful, and also the source of many creative compilation errors over the years.
#embed is different.
Instead of including a file as text, #embed allows us to load a file at compile time as data.
That data can be text, binary, configuration, a schema, a small asset, a protocol description, or anything else we want to ship into the program during compilation.
Conceptually, you can think about it like this:
constexpr unsigned char data[] = { #embed "user.schema" , '/0'};The file is embedded into the program as bytes.
That is a very important difference.
With #include, the file becomes part of your source code.
With #embed, the file becomes data that your program can inspect, parse, validate, or transform.
Why is this useful?
Because many programs depend on external descriptions of data.
For example:
Network message schemas
Serialization formats
Protocol definitions
Configuration files
Binary resources
Test data
Small lookup tables
Generated metadata
Traditionally, we often handle this in one of three ways.
We either parse the file at runtime, write a code generator that runs before compilation, or use macros and includes to bend the preprocessor until it starts crying.
All of these options work.
But they all have tradeoffs.
Runtime parsing adds runtime cost and moves errors later.
External code generation adds another build step and another tool to maintain.
Macros are macros. Enough said.
With #embed, we get another option:
Put the file into the compilation process as data, then let C++ code process it.
And with constexpr, consteval, and now reflection, that processing can happen at compile time.
A small example: schema-driven types
Imagine we have a small schema file that describes a user message:
int id;
std::string name;This file is not C++ source code that we want to include directly.
It is a tiny schema.
It says:
A message has an id field of type int, and a score field of type double.
Now imagine our C++ code does the following:
Uses #embed to load the schema file at compile time.
Parses the schema during compilation.
Extracts the field names and field types.
Uses reflection to create a matching structure.
Adds internal fields that the user did not write in the schema.
For example, the we will use the user schema we have seen in the beginning of this paragraph
but the end struct should be
struct user_message {
int id;
std::string name;
std::span<std::byte> payload;
};The user only describes the public protocol fields.
The system adds what it needs internally.
That could be a payload, metadata, versioning information, checksums, tracing IDs, serialization helpers, or anything else required by the communication layer.
The important part is that the schema stays simple, while the C++ code can build richer types around it.
Why combine #embed with reflection?
#embed gives us the data.
Reflection gives us the ability to reason about C++ entities at compile time.
Together, they open the door to a very powerful style of programming:
Use external data to guide compile-time C++ structure generation and validation.
That means we can move more logic from runtime to compile time.
For example, we can check that a schema is valid before the program is even built.
We can reject unsupported types.
We can detect duplicate field names.
We can generate helper code.
We can make sure the wire format and the C++ type match.
And we can do all of that before the first request reaches production.
Which is always nice, because production is a terrible place to discover that your message format was “almost correct”.
Why this matters for communication protocols
Communication protocols are a great example.
Very often, we have data that arrives from the network in a known format.
We need to parse it, validate it, transform it, and map it into C++ structures.
If the message layout is described in a schema, then that schema becomes the source of truth.
Using #embed, we can bring that schema into the program at compile time.
Using reflection, we can use it to build or adapt the C++ representation.
This means we can potentially get:
Better validation
Less handwritten boilerplate
Fewer mismatches between schema and code
Earlier errors
More optimized generated structures
Cleaner protocol evolution
The schema describes what the user wants.
The compiler helps us build what the program needs.
That is a very nice separation.
But please use this carefully
As always with powerful C++ features, the fact that we can do something does not mean we should immediately use it everywhere.
Compile-time parsing and reflection-based generation can be amazing.
They can also become a very creative way to make error messages that look like encrypted messages from another civilization.
So we still need to be careful.
Good use cases are places where compile-time validation really matters, where the schema is small enough to reason about, and where the generated structure removes real boilerplate.
Bad use cases are places where we are just trying to be clever.
C++ already gives us enough ways to be clever.
We do not need to unlock all of them in the same file.
The bigger picture
For me, the exciting part is not only #embed itself.
The exciting part is the direction.
Modern C++ is slowly giving us better tools to move work into compile time in a structured way.
Not only with templates.
Not only with macros.
But with real language facilities.
We already have powerful constexpr.
We are getting reflection.
We are getting #embed.
Together, these features allow us to build systems where the compiler can validate more, prepare more, and generate more.
That can lead to safer code.
Cleaner abstractions.
Less runtime overhead.
And fewer boring manual transformations.
Which is great, because if I have to write another field-mapping function by hand, I may start reflecting on my life choices.
Pun intended.
Sadly.
Final thoughts
#embed allows us to load files at compile time as data.
Reflection allows us to inspect and create C++ structures at compile time.
Together, they give us a powerful new way to connect external descriptions, like schemas or protocol definitions, with real C++ types.
In the example I am working on, the schema describes a struct with field names and types.
The C++ code embeds that schema, parses it, creates a new struct at compile time, and even adds an extra payload field that was not provided by the user.
The possibilities are huge.
Schema validation.
Protocol generation.
Embedded configuration.
Static resources.
Compile-time metadata.
And probably many ideas we have not abused yet.
Used correctly, this is an amazing direction for C++.
Keep learning, keep experimenting, and remember:
The compiler is not only here to judge you 😉
Most of the time, it is here to make your code better.



