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:
-
std::define_static_arraypromotes the compile-time vector returned bynonstatic_data_members_ofinto static storage. The expansion statement needs a range whose elements are usable as constant expressions. -
constexpr auto mis a new reflection value per iteration. It is not a runtime loop variable. -
obj.[:m:]is a member access splice. The generated code accesses the actual member; there is no string-to-member lookup at run time.
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:
-
Reflect. Start from
^^Tor^^member. -
Query. Ask
std::metafor members, names, types, bases, enumerators, properties. -
Decide. Use ordinary
if, loops andconstevalfunctions. This is normal compile-time code, not a special DSL. - 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
- Reflect a constructor or destructor and splice it as an expression. Constructors and destructors are not spliceable expressions.
- Reflect a local entity and use it outside its scope. Reflection does not extend lifetimes.
-
Use
define_aggregateon a class that is already complete, or on a class with bases, virtual functions or user-declared constructors. - Query members of an incomplete type. The compiler must know the member list; a forward declaration is not enough.
-
Treat
std::meta::infoas a runtime value that can be serialized and reloaded. It is a compile-time handle, not a portable descriptor.
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.