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:

  1. 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 by consteval functions.
  2. 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:

  1. ^^E reflects the enum type. std::meta::enumerators_of returns a list of reflections, one per enumerator.
  2. template for unrolls the loop at compile time. Each e is a constexpr reflection value.
  3. [:e:] splices the enumerator back into an expression, so value == [: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:

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

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:

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.