Ch24.1: Why Reflection
1. The Question the Compiler Cannot Answer
Suppose you have this type:
struct person
{
std::string name;
int age;
double height;
};
You want to print it as JSON:
{"name":"Ada","age":36,"height":1.68}
In most languages this is a library call. In C++ before C++26, it is manual work. The compiler knows the member names, the member types, their order and their layout. The program cannot ask for any of it. The information exists during translation and disappears from the language's expression grammar.
The usual workarounds all move the knowledge somewhere else:
| Workaround | Where the knowledge lives | What goes wrong |
|---|---|---|
| Hand-written serializer | A second function next to the type | It drifts. Add a field and forget the serializer; the bug is silent. |
| Macro list | A textual list expanded into both the struct and the visitor | It is still a second declaration. It cannot see types, bases or attributes. |
| External code generator | A build step that parses the header | Another parser, another build dependency, another source of stale output. |
| Compiler-specific intrinsic | __builtin_... or a plugin |
Not portable, not standard, often not available on every target. |
| Runtime type information | RTTI, debug info, a registration macro | Runtime cost, no member names, and only for polymorphic types. |
Reflection is the missing operation: a program asking the compiler about the program it is compiling. It is not a runtime library and it is not a debugger. It is a compile-time query whose answers can be turned back into code.
2. Compile-Time, Not Runtime
Java and C# have runtime reflection: an object carries a class descriptor, and the program can enumerate fields, call methods and create instances while it runs. C++26 reflection is different. It happens during compilation, in the constant evaluator. The result is either a constant value or generated code. By the time the program runs, the reflection is gone unless you explicitly generated a table.
| Property | Runtime reflection (Java/C#) | C++26 reflection |
|---|---|---|
| When it runs | At run time | During translation, in consteval |
| What it can see | Loaded types and metadata | Anything the compiler can name in this translation unit |
| Cost at run time | Metadata, lookups, allocations | None if the generated code is a direct access |
| Can it create new types? | Usually not | Yes, a simple aggregate through define_aggregate |
| Can it see private members? | With enough privilege | Only through an explicit access context |
The compile-time restriction is a feature, not a limitation. The compiler already has the complete type information; asking at compile time means the answer can be type-checked, inlined and optimized. There is no string lookup at run time and no metadata blob in the binary.
3. Reflection and Generation Are One Feature
C++26 reflection is really two capabilities joined at the hip:
-
Reflection: turn a program entity into a value of type
std::meta::info. The value can be stored, compared, put in a vector, and queried byconstevalfunctions. - Splicing and generation: turn a reflection back into a grammatical element — a type, a name, an expression, a member access — or define a new aggregate from a list of reflected members.
The first capability without the second would be a compile-time query language with no way to use the answer. The second without the first would be a macro system with better types. The combination is what makes the JSON example possible: reflect the members, query their names and types, then generate the field accesses.
grammar ──^^──► reflection value ──std::meta::...──► new reflection value
▲ │
└────────────────────── [: ... :] ───────────────────────────┘
The top arrow is the reflection operator ^^. The bottom arrow is a
splice: [: r :] inserts the entity represented by
r back into the program. The middle is ordinary consteval code:
loops, if, std::vector, functions.
4. A First Look: Enum to String
The classic motivating example is an enum whose name you want to print. Before reflection, every project had a switch, a macro table, or a third-party library that parsed the enum declaration.
enum class color { red, green, blue };
template <typename E>
requires std::is_enum_v<E>
constexpr std::string_view enum_to_string(E value)
{
template for (constexpr auto e :
std::define_static_array(std::meta::enumerators_of(^^E)))
{
if (value == [:e:])
{
return std::meta::identifier_of(e);
}
}
return "<unnamed>";
}
static_assert(enum_to_string(color::green) == "green");
Do not worry about the syntax yet. Look at the three steps:
-
^^Ereflects the enum type.std::meta::enumerators_ofreturns a list of reflections, one per enumerator. -
template forunrolls the loop at compile time. Eacheis aconstexprreflection value. -
[:e:]splices the enumerator back into an expression, sovalue == [:e:]is a normal comparison.identifier_of(e)gives the spelling.
The compiler generates one comparison per enumerator. There is no table lookup, no string parsing, no registration. If you add an enumerator, the function sees it automatically.
5. Why Templates and Type Traits Are Not Enough
C++ already has compile-time computation. Templates are Turing-complete, and
<type_traits> answers hundreds of questions. Why is that not reflection?
A type trait answers a question the library author anticipated:
is_integral_v<T>, is_base_of_v<A, B>,
tuple_size_v<T>. It cannot answer "what are the member names of
T, in declaration order?" because there is no syntactic form for that answer.
The trait would have to return a list of strings and types, and the language had no type that
could hold a heterogeneous list of program entities.
Templates also cannot generate a new type from a runtime-looking list. You can specialize a
template on a parameter pack, but you cannot write a loop that pushes a
data_member_spec into a vector and then defines a struct from it. That is exactly
what std::meta::info and define_aggregate add.
Macros are textual, not semantic. They see tokens, not types. A macro cannot ask whether a member is a bit-field, whether a base is virtual, or what the underlying type of an enum is. It also cannot iterate over the members of a type it did not declare.
6. What Reflection Is Good For
The motivating use cases are boring and everywhere:
- Serialization. JSON, YAML, binary formats, database rows. Reflect the members once; generate the field accesses.
- Enum names. Logging, command-line parsers, configuration files, protocol dumps.
- Bindings. C++ to Python, Lua, JavaScript, C ABI. Generate the wrapper from the C++ declaration instead of maintaining it by hand.
- Testing and mocking. Generate a structural equality, a hash, a debug printer, or a builder from the members.
- Layout and ABI tools. Inspect offsets, sizes and alignments at compile time; generate a static assertion or a binary layout description.
- Command-line and configuration parsers. Derive option names from member names and types from member types.
The common thread is a declaration that already contains the information, and a second declaration that repeats it. Reflection deletes the second declaration. That is the entire value proposition.
7. What Reflection Is Not
-
It is not runtime introspection. You cannot reflect a type that was
loaded from a shared library at run time, and you cannot ask for a member name from a
void*. - It does not replace templates. Templates instantiate code over types; reflection inspects and generates declarations. They compose: reflection can produce the arguments for a template instantiation.
-
It does not make private members public. Access control still applies.
Member queries take an
access_context; asking from the wrong context is an error, not a loophole. - It does not remove the need to design an API. A generated serializer still needs a policy for optional fields, versions, unknown keys and errors. Reflection removes the boilerplate, not the design.
- It is not free of compile-time cost. Every query and every generated declaration is work the compiler does. A reflection-heavy header can slow a build. The cost is at compile time, not run time.
8. The State of the Standard
Reflection was voted into C++26 in June 2025 as P2996, together with supporting papers for
annotations, consteval blocks, expansion statements and the <meta>
library. The language features are:
- the reflection operator
^^; - the opaque type
std::meta::infoand thestd::metanamespace; - the splice specifiers
[: ... :],typename[: ... :],template[: ... :]; template forexpansion statements;consteval { ... }blocks and injected declarations;std::meta::define_aggregateanddata_member_spec.
Compiler support is still uneven. GCC 16 has an implementation; Bloomberg's
clang-p2996 fork is the reference Clang implementation; EDG has one; MSVC had no
public implementation at the time of writing. The code in this chapter is C++26, not a
library emulation. Check the compiler support table before you put it in a production build.
The rest of the chapter teaches the model, then the patterns. The syntax looks strange at
first because it is new. The mental model is small: reflect with ^^, query with
std::meta, splice with [: :], generate with
define_aggregate.