C++ Module 18 – C++26 previews: reflection, contracts and safety
C++26 reflection: enum names and serialisation
Use C++26 reflection to turn enums into strings and back, write structs out as JSON and keep a portable fallback for compilers without reflection.
What you will learn
- Convert enums to and from strings without hand-written tables
- Serialise aggregates generically with template for
- Fix the static constexpr error that GCC 16 gives in expansion statements
- Write a portable fallback for compilers without reflection
Before you start
What you’ll learn · Before you start
What you will learn
- Convert enums to and from strings without hand-written tables
- Serialise aggregates generically with template for
- Fix the static constexpr error that GCC 16 gives in expansion statements
- Write a portable fallback for compilers without reflection
Before you start
On this page
Two jobs come up in almost every C++ code base: turning an enum such as the four modes of a PaymentMode into names
and back, and writing a struct out as text, such as JSON. Before C++26, both needed a list that repeated the
enumerators or the members by hand, and the list went stale whenever someone added a field. Reflection removes the
list: std::meta::enumerators_of and std::meta::nonstatic_data_members_of hand you the enumerators and members
while the program compiles, and template for repeats code once for each of them. This lesson builds
enum_to_string, string_to_enum and a JSON writer that way, then shows the error GCC 16 gives for one common way of
writing the loop, and a fallback that keeps the same code building on compilers without reflection.
Preview: GCC 16 only
Status: in the final C++26 draft. The reflection programs here need GCC 16.2 and -std=c++26 -freflection, while
the fallback near the end also builds with Apple clang 21, which has no reflection.
Two generated functions turn enums into names and back
// Enum to string and string to enum, both generated from the enum itself.
#include <format>
#include <meta>
#include <optional>
#include <print>
#include <string_view>
#include <type_traits>
enum class PaymentMode { upi, card, net_banking, cash_on_delivery };
template <typename E>
requires std::is_enum_v<E>
constexpr std::string_view enum_to_string(E value) {
template for (constexpr std::meta::info e : std::define_static_array(std::meta::enumerators_of(^^E))) {
if (value == [:e:]) return std::meta::identifier_of(e);
}
return {}; // a value that no enumerator has
}
template <typename E>
requires std::is_enum_v<E>
constexpr std::optional<E> string_to_enum(std::string_view text) {
template for (constexpr std::meta::info e : std::define_static_array(std::meta::enumerators_of(^^E))) {
if (text == std::meta::identifier_of(e)) return [:e:];
}
return std::nullopt; // no enumerator has this name
}
int main() {
for (PaymentMode mode : {PaymentMode::upi, PaymentMode::net_banking, PaymentMode::cash_on_delivery}) {
std::println("{} -> \"{}\"", static_cast<int>(mode), enum_to_string(mode));
}
for (std::string_view text : {"card", "cheque", "Card"}) {
std::optional<PaymentMode> mode = string_to_enum<PaymentMode>(text);
std::println("\"{}\" -> {}", text, mode ? std::format("{}", static_cast<int>(*mode)) : "no such mode");
}
static_assert(string_to_enum<PaymentMode>("card") == PaymentMode::card); // also works while compiling
static_assert(enum_to_string(PaymentMode::upi) == "upi");
} from session import run
run("""
g++-16 -std=c++26 -freflection -Wall -Wextra enum_strings.cpp -o enum_strings && ./enum_strings
""") Output
$ g++-16 -std=c++26 -freflection -Wall -Wextra enum_strings.cpp -o enum_strings && ./enum_strings 0 -> "upi" 2 -> "net_banking" 3 -> "cash_on_delivery" "card" -> 1 "cheque" -> no such mode "Card" -> no such mode
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 enum_session.py
Both functions loop over the same reflected list. enum_to_string compares the value with each enumerator and
returns the first name that matches; string_to_enum compares the text with each name and returns the enumerator,
spliced back in with [:e:]. It returns std::optional<E>, because text from a file or a request may name no
enumerator at all, as “cheque” shows. The comparison is exact, so “Card” is not card: decide on purpose whether
your inputs should be matched without regard to case, and convert them first if so.
The two static_assert lines at the end show that both functions also run while compiling, so a name can be checked
before the program exists. Nothing in either function names PaymentMode’s enumerators, so adding wallet to the
enum updates both directions at once. Without reflection, the older ways remain: an
A list of items written once inside a macro, then expanded more than once with a different definition of the item macro, for example to declare an enum and to write out its names.
list that generates the enum and its names, a hand-written switch, or a script that writes the code at build time.
Checkpoint
Check: Enum names in both directions
template for stamps out one copy of its body per element
The figure follows the loop of to_json, the function of the next section, over the four members of a Payment:
An expansion statement is unrolled while compiling, one copy of its body per element
Text description of the diagram
The diagram shows how the loop in to_json is expanded, from top to bottom.
1. What you write: template for (constexpr info m : members), with a body that writes the name of m and then object.[:m:]. 2. An arrow labelled "expanded while compiling" leads to a box titled "One copy per member", which holds four boxes in order: - copy 1: m is ^^Payment::id; the body writes "id" and then object.id, an int; - copy 2: m is ^^Payment::payer; it writes "payer" and object.payer, a std::string; - copy 3: m is ^^Payment::amount_rupees; it writes "amount_rupees" and object.amount_rupees, a double; - copy 4: m is ^^Payment::refunded; it writes "refunded" and object.refunded, a bool. 3. An arrow leads from the last copy to a double-bordered box: in the running program there are four member accesses in a row, as if written by hand, with no loop and no reflections left.
template for is not a loop that runs. The compiler makes one copy of its body for each element of the range, with
the element as a constant, and compiles the copies one after another, as the diagram shows for the four members of
a Payment. That is why each copy can splice its member and use the member’s own type: the copy for id formats an
int, the copy for payer a std::string, and an ordinary loop could do neither. It is also why a loop over a
large enum makes a longer function: twenty enumerators mean twenty copies of the comparison.
Inside the copies, break and continue behave as they do in a loop: when the program runs, break skips all the
remaining copies and continue jumps to the next one, while a return leaves the function at once. That is how
enum_to_string stops at the first match, and it costs nothing, since the copies are ordinary code that runs one
after another.
Key idea
template for is unrolled while compiling: one copy of its body per element, with that element as a constant of
its own type, which is what lets each copy splice it.
Checkpoint
Check: How template for expands
One generic function writes a flat struct as JSON
// A struct written out as JSON, with one generic function and no list of members.
#include <format>
#include <meta>
#include <print>
#include <string>
#include <type_traits>
struct Payment {
int id;
std::string payer;
double amount_rupees;
bool refunded;
};
void write_value(std::string& out, const auto& value) {
using T = std::remove_cvref_t<decltype(value)>;
if constexpr (std::is_same_v<T, bool>) {
out += value ? "true" : "false";
} else if constexpr (std::is_arithmetic_v<T>) {
out += std::format("{}", value);
} else {
out += std::format("\"{}\"", value); // text, without escaping yet
}
}
template <typename T>
std::string to_json(const T& object) {
std::string out = "{";
bool first = true;
template for (constexpr std::meta::info m :
std::define_static_array(std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::current()))) {
out += first ? "" : ",";
first = false;
out += std::format("\"{}\":", std::meta::identifier_of(m));
write_value(out, object.[:m:]);
}
return out + "}";
}
int main() {
std::println("{}", to_json(Payment{1, "Asha", 1299.5, false}));
std::println("{}", to_json(Payment{2, "Ravi", 49.0, true}));
} from session import run
run("""
g++-16 -std=c++26 -freflection -Wall -Wextra json_flat.cpp -o json_flat && ./json_flat
""") Output
$ g++-16 -std=c++26 -freflection -Wall -Wextra json_flat.cpp -o json_flat && ./json_flat
{"id":1,"payer":"Asha","amount_rupees":1299.5,"refunded":false}
{"id":2,"payer":"Ravi","amount_rupees":49,"refunded":true}
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 json_flat_session.py
Step through the four copies for the first payment and watch out grow:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- One copy per member. to_json is compiled for T = Payment, and template for over its four members becomes four copies of the loop body before the program runs. When it runs, out starts as "{" and first is true.
- Copy 1: an int. first is true, so no comma is written, and first becomes false. The copy writes "id": and then object.id, an int, which write_value turns into digits: out is now {"id":1.
- Copy 2: a string. From here on first is false, so each copy starts with a comma. object.payer is a std::string and takes the text branch: "Asha" in quotation marks, not yet escaped.
- Copy 3: a double. object.amount_rupees is a double. std::format writes the shortest text that reads back as the same value, here 1299.5; the second payment's 49.0 comes out as 49, which JSON reads as the same number.
- Copy 4: a bool. object.refunded is a bool, which the first branch of write_value catches with if constexpr: it writes false, a JSON literal, without quotation marks.
- The closing brace. After the four copies, to_json returns out + "}". The first line of the run is {"id":1,"payer":"Asha","amount_rupees":1299.5,"refunded":false}, from one generic function and no list of names.
to_json writes {, then a "name":value pair for each public member, then }. write_value picks the format
from the member’s type with if constexpr: true or false for a bool, digits for numbers, and quotation marks
for text. std::format writes a double in the shortest form that reads back as the same value, so 49.0 becomes
49, which JSON accepts as a number. The members come from access_context::current(), so private members of a
class are left out, as they should be in anything written to a file or sent over a network.
This version is deliberately small: it handles flat structs whose text contains no quotation marks. Real data
breaks it in three ways, all of them common: a name such as Asha "A." Rao produces broken JSON, a nested struct
does not compile, and a double that is not a number has no JSON form at all.
Checkpoint
Check: A flat struct written as JSON
Practise: names, copies and JSON
Nine questions on enum names, the copies of template for and the flat writer follow, starting with the idea you know least.
Practice that adapts to you
Practise this lesson
9 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
A JSON writer for nested structs, vectors and awkward text, checked by a parser Pro: opens with a Pro, Premium or Ultimate pass
This part grows to_json into a writer that handles nesting, vectors and any text, and proves its output with an
independent JSON parser. It ends with an exercise that adds optional values and maps.
In this part:
- Recursion, vectors and escaping in one header
- The output, checked by Python’s JSON parser
- A char member that breaks the output, reproduced
- Your turn: optional values and maps
7 runnable examples (C header, 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).
The static constexpr error explained, and one code base for every compiler Pro: opens with a Pro, Premium or Ultimate pass
One way of writing the member loop fails on GCC 16 with an error that seems to say the opposite of the standard. This part reproduces it, explains it, and then builds the enum names on compilers that have no reflection at all.
In this part:
- The error GCC 16 gives without static, reproduced
- One code base for every compiler: reflection where available, an X-macro elsewhere
7 runnable examples (C header, C++, Python) · about 8 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
What does template for do, and why can an ordinary for loop over a vector of reflections not do the same job?
template for is an expansion statement: while compiling, it makes one copy of its body for each element of a range, with that element available as a constant in its copy. Reflection needs exactly that, because a splice such as object.[:m:] requires a constant reflection, and each copy may use a member of a different type. An ordinary for loop runs when the program runs, and its loop variable is not a constant, so it can neither splice nor hold a std::meta::info, which exists only while compiling. Inside the copies, break and continue still work as in a loop.
-
Mid to senior
You are writing string_to_enum for enums read from configuration files that people edit by hand. What should it return for unknown text, how do you handle case and old names, and what changes for enums whose values are combined as bit flags?
-
Mid to senior
Inside a function, code stores the result of std::define_static_array, the list of a struct's members, in a local constexpr variable and loops over it with template for. GCC 16 rejects it with an error about the variable's address. Is the code wrong? Explain, and give two fixes.
-
Senior to staff
You maintain a logging library used by teams on GCC 16, Apple clang and MSVC. You want enum values in log lines to print as names. How do you design this so it works on every compiler today and improves as compilers add reflection?
Key takeaways
enumerators_ofandnonstatic_data_members_ofmake tables of names unnecessary: enum conversions and writers update themselves when a type changes.string_to_enumreturnsstd::optional, because text from outside may name no enumerator.template formakes one copy of its body per element while compiling, so each copy can use its element’s own type.- A generic writer should use
access_context::current(), so that private members stay out of files and messages. - Reflection is GCC-only for now: keep a fallback, or build reflection code with GCC 16 alone.
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)
- P2686R4: constexpr structured bindings and references to constexpr variables (ISO/IEC JTC1/SC22/WG21)
- Working Draft, Programming Languages: C++ (N5050) (ISO/IEC JTC1/SC22/WG21)
- RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format (RFC Editor (IETF))
- GCC 16 Release Series, Changes, New Features, and Fixes (GNU Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress