C++ Module 18 – C++26 previews: reflection, contracts and safety
C++26 reflection basics: ^^ and splicing
Reflect types, members and enumerators with the C++26 ^^ operator, query them with std::meta functions and splice them back into code with [: :].
What you will learn
- Reflect types, members and enumerators with the ^^ operator
- Query reflections with std::meta functions such as identifier_of and enumerators_of
- Use splices, [: :], to turn reflections back into code
- Explain why std::meta::info values exist only while a program compiles
Before you start
What you’ll learn · Before you start
What you will learn
- Reflect types, members and enumerators with the ^^ operator
- Query reflections with std::meta functions such as identifier_of and enumerators_of
- Use splices, [: :], to turn reflections back into code
- Explain why std::meta::info values exist only while a program compiles
Before you start
On this page
Before C++26, printing the four members of an Order struct meant writing every name a second time by hand, and the
copy went stale the day someone added a member. Reflection lets a C++26 program examine its own declarations while
it compiles. The operator ^^ turns a declaration, such as a type, a data member, an enumerator, a function or a
namespace, into a value of type std::meta::info. Functions in the namespace std::meta answer questions about that
value: its name, its type, its members. A splice, written [: r :], turns a reflection back into the code it
stands for. All of this happens at compile time: a std::meta::info value never exists in the running program, which
receives only the results, such as names and numbers. With GCC 16 you need -std=c++26 -freflection; Apple clang 21
does not implement reflection yet.
Preview: GCC 16 only
Status: in the final C++26 draft. All examples on this page ran on GCC 16.2 with -std=c++26 -freflection. Apple
clang 21 cannot build them; the next lesson adds a portable fallback.
Every use of reflection reflects, queries and splices
Follow enum_name(Colour::green) from the source down to the running program:
Reflection happens while the program compiles; the program keeps only its results
Text description of the diagram
The diagram shows five steps from top to bottom.
- Your code declares enum class Colour with the enumerators red, green and blue.
- An arrow labelled ^^ leads into a box titled "While the program compiles". Its first step: ^^Colour is a value of type std::meta::info that describes the enum.
- Next, a query: enumerators_of(^^Colour) gives three reflections, one each for red, green and blue.
- Last inside the box, a splice and a name: [:e:] turns a reflection back into Colour::green, and identifier_of(e) gives the string "green".
- An arrow labelled "compiled into the program" leads out of the box to a double-bordered box: when the program runs, only ordinary values are left, the comparisons and the string "green". No std::meta::info value exists at run time.
Every use of reflection follows the same three moves. You reflect a declaration with ^^. You query the
reflection with std::meta functions, which are consteval, so they run only inside the compiler: enumerators_of
lists an enum’s enumerators, nonstatic_data_members_of a class’s data members, identifier_of gives a name as
written in the source, and type_of gives the type of a member. Then you splice results back into code with
[: :], or keep plain values such as strings, which do survive into the running program.
A reflection is an ordinary value in every other way. You can store it in a constexpr variable, pass it to a
consteval function, compare two of them with == and keep many in a vector. Because the work is finished before
the program exists, the code it produces is the code you would have written by hand, and it costs nothing extra
when the program runs.
Two more pieces make loops over query results possible. A query returns a std::vector of reflections, which lives
only during constant evaluation: The compiler running code while it compiles, to find the value of a constant expression: the initialiser of a constexpr variable, a static_assert condition, an array bound. Only the result goes into the program, and the compiler caps how long it may run. More in “Compile-time programming with constexpr”, so std::define_static_array copies it into an
array with static storage duration: The lifetime of a global, a variable at namespace scope or a static variable: it exists for the whole run of the program. Variables with thread storage duration, declared thread_local, exist once per thread instead. More in “C++ const, constexpr, consteval and constinit”. An
expansion statement, template for, then repeats its body once for each element, with each element available
as a constant.
Checkpoint
Check: Reflect, query, splice
A first program finds an enumerator’s name from its value
// The name of an enumerator, from its value, with no table to keep in step.
#include <meta>
#include <print>
#include <string_view>
enum class Colour { red, green, blue };
template <typename E>
constexpr std::string_view enum_name(E value) {
// ^^E reflects the enum; enumerators_of lists its enumerators while the program compiles.
template for (constexpr std::meta::info e : std::define_static_array(std::meta::enumerators_of(^^E))) {
if (value == [:e:]) { // [:e:] splices the enumerator back in: Colour::red, ...
return std::meta::identifier_of(e); // its name, as written in the source
}
}
return "<unnamed>";
}
int main() {
std::println("{}", enum_name(Colour::green));
std::println("{}", enum_name(Colour::blue));
std::println("{}", enum_name(static_cast<Colour>(7))); // a value with no enumerator
std::println("{}", std::meta::display_string_of(^^Colour));
} from session import run
# enum_name.cpp with GCC 16: reflection needs -std=c++26 and -freflection.
run("""
g++-16 -std=c++26 -freflection -Wall -Wextra enum_name.cpp -o enum_name && ./enum_name
""") Output
$ g++-16 -std=c++26 -freflection -Wall -Wextra enum_name.cpp -o enum_name && ./enum_name green blue <unnamed> Colour
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 enum_session.py
Step through what the compiler makes of enum_name(Colour::green), then the run:
The drawing shows the whole scene; its 8 steps are listed below.
Every step as text
- ^^E reflects the enum. main calls enum_name(Colour::green), so the compiler instantiates the template with E = Colour. ^^E turns that type into a std::meta::info value, a handle that exists only inside the compiler.
- The query lists 3 names. enumerators_of(^^E) is consteval, so it runs inside the compiler. It returns a std::vector of three reflections, one each for red, green and blue, in the order they were declared.
- A static copy. That vector is freed when the constant evaluation that made it ends. define_static_array copies the three reflections into an array with static storage, which template for can walk.
- Copy 1: red. template for stamps out one copy of its body for each element. In the first copy e reflects red, so [:e:] is Colour::red and the test reads value == Colour::red.
- Copy 2: green. In the second copy [:e:] is Colour::green, and identifier_of(e) gives "green", the name exactly as written in the source, as a std::string_view.
- Copy 3, then the end. The third copy tests Colour::blue. After the copies comes return "<unnamed>", for a value that no enumerator has. The compiler is done, and no reflection is left.
- The program runs. At run time enum_name(Colour::green) is three ordinary comparisons. The first fails, the second matches, and the function returns "green", the first line the run printed.
- A value with no name. enum_name(static_cast<Colour>(7)) fails all three comparisons and reaches the last return, so the third line of the run is <unnamed>: the case a hand-written switch tends to forget.
enum_name reflects whatever enum it is given, ^^E, and loops over its enumerators. For each one, [:e:] splices
the enumerator back in, so the comparison is value == Colour::red, then value == Colour::green, and so on. The
first match returns identifier_of(e), the name exactly as written. A value that names no enumerator, such as
static_cast<Colour>(7), falls through to "<unnamed>", which is the case a hand-written switch usually forgets.
The last line shows display_string_of, a second way to get a name: it is meant for people, and its wording is up
to the compiler.
Why template for and not an ordinary for loop? A splice needs a constant reflection, but the variable of an
ordinary loop changes while the program runs. An expansion statement instead stamps out one copy of its body for
each enumerator, as if you had written three if statements by hand, and in each copy e is a constant. The
function works for any enum without change, and adding an enumerator needs no edit anywhere else: there is no table
of names to keep in step with the enum.
Checkpoint
Check: An enumerator's name from its value
The same pattern lists a struct’s members
The same pattern lists the data members of a class, and splicing a member reflection into object.[:m:] reads that
member:
// Every data member of a struct, with its name and value, from one generic function.
#include <meta>
#include <print>
#include <string>
struct Order {
int id;
std::string customer;
double amount_rupees;
bool paid;
};
template <typename T>
void print_members(const T& object) {
constexpr auto ctx = std::meta::access_context::unchecked(); // all members, whatever their access
template for (constexpr std::meta::info m : std::define_static_array(std::meta::nonstatic_data_members_of(^^T, ctx))) {
std::println("{} = {}", std::meta::identifier_of(m), object.[:m:]); // object.[:m:] is object.id, ...
}
}
int main() {
print_members(Order{1042, "Asha", 1299.5, true});
} from session import run
run("""
g++-16 -std=c++26 -freflection -Wall -Wextra members.cpp -o members && ./members
""") Output
$ g++-16 -std=c++26 -freflection -Wall -Wextra members.cpp -o members && ./members id = 1042 customer = Asha amount_rupees = 1299.5 paid = true
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 members_session.py
Each copy splices one member, with its own type:
| Copy | object.[:m:] becomes |
Printed |
|---|---|---|
| 1 | object.id, an int |
id = 1042 |
| 2 | object.customer, a std::string |
customer = Asha |
| 3 | object.amount_rupees, a double |
amount_rupees = 1299.5 |
| 4 | object.paid, a bool |
paid = true |
nonstatic_data_members_of needs an access context, which decides whether private members are included.
access_context::unchecked() returns all of them; access_context::current() returns only the members that the code
at that point may access, as a normal member access would. Each object.[:m:] becomes object.id,
object.customer and so on, with its real type, so std::println formats the int, the std::string, the
double and the bool correctly without any casts.
The members arrive in declaration order, the order of the struct’s layout. A member whose type std::println cannot
format, such as a nested struct, stops the build with a formatter error; the next lesson handles nesting by calling
the same function again for such members.
Checkpoint
Check: Members read through splices
Practise: reflect, query and splice
Ten questions on the three ideas above follow, and the idea you have answered worst so far comes first.
Practice that adapts to you
Practise this lesson
10 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
The std::meta queries and the four splice forms, each one run Pro: opens with a Pro, Premium or Ultimate pass
A handful of queries answer most questions about a type, and a splice can stand in four places in code. This part runs each of them on GCC 16.2 and shows where the compilers’ own wording leaks into the results.
In this part:
- What std::meta reports for one struct, with the numbers
- When a reflection has no name: an error reproduced and two fixes
- Four kinds of splice, and where typename is required
- What current(), unprivileged() and unchecked() let a query see
10 runnable examples (C++, Python) · about 5 minutes
Opening this part with your pass…
This part opens with any MySmartCoPilot pass.
- Pro pass · 1 week₹79$2.99
- 30 days₹179$5.99
- 1 year₹1,699$45
One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).
Why reflections never reach run time: the errors reproduced, and an exercise Pro: opens with a Pro, Premium or Ultimate pass
Every reflection value has to be used up while the program compiles. Six short programs show the errors GCC gives when that rule is broken, how to fix them, and how reflections compare.
In this part:
- A reflection chosen at run time, and the fix
- How reflections compare: aliases are entities of their own
- Your turn: member_count and describe
11 runnable examples (C header, C++, Python) · about 6 minutes
Opening this part with your pass…Pro part. It opens with the same pass as the part above.
Interview questions
-
Fresher to mid · Warm-up
Why can a std::meta::info value not be used when the program runs?
std::meta::info is a consteval-only type: its values describe declarations inside the compiler, so they exist only while the program compiles. An object of that type may be a constexpr variable, a template argument or a value inside one constant evaluation, and the std::meta query functions are consteval. Code that needs reflection results at run time must turn them into ordinary values first, such as the string_view that identifier_of returns, a count, or code produced by a splice. That is also why the loop over members is an expansion statement, template for, which repeats code while compiling, instead of an ordinary for loop.
-
Mid to senior
A generic logger prints every data member of any struct by reflection, using nonstatic_data_members_of with access_context::unchecked(). A security audit flags it before release. What is the risk, and how do you change the logger and its tests?
-
Mid to senior
A serialiser writes each object's type name, taken from display_string_of, into saved files, and checks it again when loading. All tests pass with GCC 16. What can go wrong in the field, and what would you store instead?
-
Senior
A unit test checks that a serialiser covers every field: it compares the number of fields written with nonstatic_data_members_of(^^T, unchecked()).size(). For a class that derives from another, the test passes while a field is missing from the output. Why, and how do you fix the test?
Key takeaways
^^reflects a declaration as astd::meta::infovalue;std::metafunctions query it;[: :]splices it back into code.- Query results are vectors of reflections:
std::define_static_arrayturns them into a static array, andtemplate forrepeats code once for each element. identifier_ofgives names as written;display_string_ofgives names for people, in the compiler’s own words.access_context::unchecked()sees private members,current()only what the calling code may access.- GCC 16 needs
-std=c++26 -freflection; Apple clang 21 has no reflection.
References
- P2996R13: Reflection for C++26 (ISO/IEC JTC1/SC22/WG21)
- P1306R5: Expansion statements (ISO/IEC JTC1/SC22/WG21)
- P3491R3: define_static_{string,object,array} (ISO/IEC JTC1/SC22/WG21)
- Working Draft, Programming Languages: C++ (N5050) (ISO/IEC JTC1/SC22/WG21)
- GCC 16 Release Series, Changes, New Features, and Fixes (GNU Project)
- Options Controlling C++ Dialect (GCC 16.2 manual) (GNU Project)
- C++ Support in Clang (LLVM Project)
Report a problem with this lesson
Kept only in this browser. Your Learn progress