Ch24.2: The C++26 Reflection Model

中文(简体)

1. Two Domains

C++26 reflection has exactly two domains:

Domain Contains How you enter it
The grammatical domain Types, names, expressions, declarations, statements, members, enumerators, templates. Everything you can write in source code. Write ordinary C++.
The reflection domain Values of type std::meta::info. One value represents one entity or expression. ^^ reflects a grammatical element into a value.

You move from grammar to reflection with ^^. You move back with a splice, [: ... :]. Everything in between is ordinary consteval code that manipulates std::meta::info values.

type T  ──^^T──►  info{^T}  ──nonstatic_data_members_of──►  [info, info, ...]
                                                        │
                                                        ▼
field access  ◄──[:m:]──  info{m}  ◄──template for──────┘

2. The Reflection Operator ^^

^^ takes a grammatical element and produces a reflection value. It is not ^ (xor) and not a unary operator on values. The syntax is:

constexpr auto r1 = ^^int;             // a type
constexpr auto r2 = ^^std::vector;     // a template
constexpr auto r3 = ^^std::vector<int>; // a specialization
constexpr auto r4 = ^^person::name;    // a non-static data member
constexpr auto r5 = ^^color::green;    // an enumerator
constexpr auto r6 = ^^std::printf;     // a function
constexpr auto r7 = ^^std;             // a namespace
constexpr auto r8 = std::meta::reflect_constant(42); // a value
constexpr auto r9 = ^^std::is_same_v<int, int>; // a variable template specialization

The result is a constant expression of type std::meta::info. It is opaque: you cannot index it, cast it to a pointer, or print it as text. You ask questions with std::meta functions. Note that ^^ reflects entities (types, names, variables, members, enumerators), not arbitrary literals; a constant value is reflected with std::meta::reflect_constant, as in r8.

std::meta::info is comparable. Two reflections of the same entity compare equal; reflections of different entities compare unequal. A default-constructed info{} is a null reflection, not a reflection of void.

static_assert(^^int == ^^int);
static_assert(^^int != ^^long);
static_assert(std::meta::info{} == std::meta::info{});

The type is deliberately opaque. The committee did not want a family of meta::type, meta::variable, meta::function types, because that would freeze the language's ontology into the type system. One type plus predicates is easier to extend.

3. The std::meta Namespace

<meta> provides consteval functions that take reflections and return reflections, values, or both. The names are long but regular. The ones you will use first:

Function Answer
type_of(r) The type of the entity or expression represented by r.
identifier_of(r) The declared name, as a std::string_view.
display_string_of(r) A human-readable spelling, not necessarily a valid identifier.
nonstatic_data_members_of(T, ctx) The non-static data members of a class, in declaration order.
enumerators_of(E) The enumerators of an enum, in declaration order.
bases_of(T, ctx) The direct base classes of a class.
members_of(T, ctx) All members: data, functions, types, constructors, and so on.
is_enum_type(r), is_class_type(r), ... Predicates on reflections.
dealias(r) The underlying entity of a type or namespace alias.
substitute(template, args) A reflection of the template specialization with the given arguments.
extract<T>(r) The value represented by a reflection of a constant, enumerator or object.
reflect_constant(v) A reflection of the constant value v.
data_member_spec(type, opts) A description of a data member to be injected by define_aggregate.
define_aggregate(T, specs) Complete the incomplete class T with the given members.

If you have used <type_traits>, many predicates will look familiar: is_integral_type, is_same_type, is_base_of_type, is_convertible_type. They are the same questions, expressed over reflections instead of template arguments.

4. Splices: Going Back to Grammar

A splice turns a reflection back into source. The context decides what kind of grammatical element is needed:

Form Produces Example
[: r :] An expression, name or value, depending on context. int x = [: r :];
typename[: r :] A type. typename[: r :] x = 42;
[: r :]::name A scope-qualified name. [: r :]::value
template[: r :]<T> A template specialization. template[: r :]<int>::value
obj.[: m :] A member access. p.[: member :] = 42;

The first example from the proposal is the whole back-and-forth:

constexpr auto r = ^^int;
typename[:r:] x = 42;          // int x = 42;
typename[:^^char:] c = '*';    // char c = '*';

A splice is not a string substitution. The compiler checks that the reflection represents the kind of entity the grammar requires. Splicing a reflection of a function where a type is required is an error, not a text replacement.

A member access splice is more direct than a name lookup. It does not perform ordinary member lookup or overload resolution; it accesses exactly the member represented by the reflection. That is why it can reach a member whose name is not known at the call site.

5. template for: Compile-Time Iteration

template for is the expansion statement. It looks like a range-based for, but it is unrolled during translation. Each iteration has its own constexpr variable.

template <typename T>
void print_members(T const &obj)
{
    constexpr auto ctx = std::meta::access_context::current();
    constexpr auto members = std::define_static_array(
        std::meta::nonstatic_data_members_of(^^T, ctx));

    template for (constexpr auto m : members)
    {
        std::print("{} = {}\n", std::meta::identifier_of(m), obj.[:m:]);
    }
}

Three details matter:

template for also works with an index and with multiple ranges:

template for (constexpr std::size_t i{}; auto m : members)
{
    std::print("{}: {}\n", i, std::meta::identifier_of(m));
}

6. Generating a Type: define_aggregate

Reflection can do more than inspect existing declarations. It can complete an incomplete aggregate from a list of member descriptions. This is the smallest form:

#include <meta>
#include <print>

struct point;

consteval
{
    std::meta::info members[]
    {
        std::meta::data_member_spec(^^int, {.name = "x"}),
        std::meta::data_member_spec(^^int, {.name = "y"})
    };

    std::meta::define_aggregate(^^point, members);
}

int main()
{
    point p{.x = 3, .y = 4};
    std::println("({}, {})", p.x, p.y);
}

point is declared incomplete. The consteval block runs during translation, builds two member descriptions, and injects a definition. From that point on, point is a normal aggregate with two int members.

The member list can be computed. A common pattern is a "struct of arrays":

#include <array>
#include <meta>
#include <vector>

template <typename T, std::size_t N>
struct struct_of_arrays
{
    struct impl;

    consteval
    {
        auto ctx = std::meta::access_context::current();
        std::vector<std::meta::info> specs;

        template for (constexpr auto m :
                      std::define_static_array(
                          std::meta::nonstatic_data_members_of(^^T, ctx)))
        {
            auto array_type = std::meta::substitute(
                ^^std::array,
                {std::meta::type_of(m), std::meta::reflect_constant(N)});
            specs.push_back(std::meta::data_member_spec(
                array_type, {.name = std::meta::identifier_of(m)}));
        }

        std::meta::define_aggregate(^^impl, specs);
    }

    impl data;
};

substitute builds the reflection of std::array<MemberType, N>; data_member_spec names it after the original member; define_aggregate completes impl. The member list uses std::vector<std::meta::info>; the std::meta queries themselves return the same type, so the example stays with the standard container.

That is also the freestanding wart. std::vector is not a freestanding container, and the range-returning metafunctions (nonstatic_data_members_of, enumerators_of, members_of, bases_of) return std::vector<std::meta::info>. P3295 proposes freestanding constexpr containers to make those functions usable in a freestanding environment, but until that is available in your standard library they are hosted-only. The core language (^^, splices, std::meta::info) is freestanding-friendly; the container-returning library functions are the part to check.

This is the kind of code that used to require an external generator. The constraints are real: generated aggregates are simple aggregates, not arbitrary classes with constructors, virtual functions or access specifiers.

7. Access Context and Value Extraction

Member queries take an std::meta::access_context. It says from where the query is made, because access control is part of the language:

constexpr auto here = std::meta::access_context::current();
constexpr auto all  = std::meta::access_context::unchecked();

current() uses the current function or class as the access context. It is the safe default: a reflection cannot see a private member that the current code could not name. unchecked() bypasses access checking and should be reserved for tools that genuinely need the layout, such as a debugger or a binary serializer that is part of the class's own implementation.

To get a value out of a reflection, use extract<T>:

enum class color { red, green, blue };

constexpr auto e = std::meta::enumerators_of(^^color)[1];
static_assert(std::meta::extract<color>(e) == color::green);

extract is also how you turn a reflection of a constant into a real value for a template argument or a switch case. It is the inverse of reflect_constant.

8. The Recipe

Every reflection algorithm in this chapter follows the same four steps:

  1. Reflect. Start from ^^T or ^^member.
  2. Query. Ask std::meta for members, names, types, bases, enumerators, properties.
  3. Decide. Use ordinary if, loops and consteval functions. This is normal compile-time code, not a special DSL.
  4. Splice or generate. Turn the result back into expressions, types, or a new aggregate.

The syntax is unfamiliar, but the control flow is not. If you can write a loop over a vector and an if on a type trait, you can write reflection code. The only new rule is that the vector holds std::meta::info values and the result must be spliced back into the grammar.

9. What You Cannot Do

The next page puts the model to work: enum names, field visitors, JSON, and generated aggregates. The code is longer, but every line is one of the four steps above.