Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Advanced
  • 25 minutes
  • Examples run with Clang 21.0.0, GCC 16.2.0 and Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 10 of 25 minutes free
Free: Two generated functions turn enums into names and back · stamps out one copy of its body per element · One generic function writes a flat struct as JSON · Practise: names, copies and JSON. With a pass: 2 in-depth parts and 3 interview answers, about 15 minutes.

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

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_strings.cpp C++ · enum_strings.cpp
// 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");
}
Three names out, three strings in Python · enum_session.py
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

  1. Question 1 of 2 Why does string_to_enum return std::optional<E> instead of E?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Text from outside the program may name no enumerator, and the caller must be able to tell

    "cheque" names no PaymentMode, so the function needs a way to say "no match" that no enumerator can be confused with. Returning a default enumerator instead would turn bad input into a valid-looking value.

  2. Question 2 of 2 string_to_enum<PaymentMode>("Card") returned no value in the run. Why?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: identifier_of gives the name exactly as written, "card", and the comparison is case-sensitive

    The names come from the source exactly as written, so "Card" and "card" are different strings. If inputs may use any case, convert them before the comparison, and decide which form to write back out.

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:

A template for over Payment’s four members becomes four copies of its body, each using one member with its own type.What you writetemplate for (constexpr info m : members){ write the name of m; write object.[:m:] }One copy per memberIn the running programfour member accessesin a row, as if writtenby hand: no loop andno reflectionscopy 1: m is idwrites "id" andobject.id, an intcopy 2: m is payerwrites "payer" andobject.payer, a stringcopy 3: m is amount_rupeeswrites its name andobject.amount_rupees,a doublecopy 4: m is refundedwrites "refunded" andobject.refunded, a boolexpanded whilecompiling

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

  1. Question 1 of 2 enum_to_string is used with an enum of twenty enumerators. How many copies of its comparison does template for make?

    Type a number.

    Show the answer: Question 1 of 2

    Answer: 20 copies

    An expansion statement makes one copy of its body per element of the range, so twenty enumerators mean twenty copies of the comparison: a longer function, made while compiling, with nothing to loop over at run time.

  2. Question 2 of 2 In a template for over four members, the body of the second copy runs continue. What runs next?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The third copy

    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, which is how enum_to_string stops at the first match. It costs nothing, since the copies are ordinary code in a row.

One generic function writes a flat struct as JSON

json_flat.cpp C++ · json_flat.cpp
// 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}));
}
Two payments, one generic function Python · json_flat_session.py
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:

Interactive · stage by stage to_json(Payment{1, "Asha", 1299.5, false}), copy by copy Step through the four copies the compiler made of the loop body. Each sub-label shows the text that copy adds to out.
to_json(Payment{1, "Asha", 1299.5, false}), copy by copy, step 6 of 6. Showing out = "{", Copy 1: id ("id":1), Copy 2: payer (,"payer":"Asha"), Copy 3: amount_rupees (,"amount_rupees":1299.5), Copy 4: refunded (,"refunded":false), return out + "}". In focus: return out + "}".out = "{"first = trueCopy 1: id"id":1Copy 2: payer,"payer":"Asha"Copy 3: amount_rupees,"amount_rupees":1299.5Copy 4: refunded,"refunded":falsereturn out + "}"one JSON object

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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

  1. Question 1 of 2 What does the flat to_json write for a payer named Asha "A." Rao?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Broken JSON, because the quotation marks inside the name end the string early

    This version is deliberately small. A name with quotation marks produces broken JSON, a nested struct does not compile, and a double that is not a number has no JSON form at all.

  2. Question 2 of 2 A class passed to to_json has a private member. Is it written?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: No; the members come from access_context::current(), which leaves out what to_json may not access

    to_json asks for members with 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.

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.

  1. Question 1 of 9 Where do the names that enum_to_string returns come from?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: identifier_of, applied to each enumerator that enumerators_of returns

    Nothing in either function names PaymentMode's enumerators, so adding wallet to the enum updates both directions at once.

  2. Question 2 of 9 What do the two static_assert lines at the end of enum_strings.cpp show?

    Choose one answer.

    Show the answer: Question 2 of 9

    Answer: Both functions also run while compiling, so a name can be checked before the program exists

    string_to_enum<PaymentMode>("card") == PaymentMode::card and enum_to_string(PaymentMode::upi) == "upi" are both evaluated by the compiler, so a wrong name would stop the build.

  3. Question 3 of 9 A team adds wallet to PaymentMode and asks what else must change for "wallet" to round-trip. What do you tell them?

    Choose one answer.

    Show the answer: Question 3 of 9

    Answer: Nothing; both functions read the enumerators from the enum

    Before reflection, the list of names was the part that went stale. With enumerators_of, the enum is the only list.

  4. Question 4 of 9 to_json loops over the four members of Payment with template for. What does the compiler make of the loop?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: Four copies of the loop body, one per member, each using that member with its own type

    An expansion statement is unrolled while compiling. Each copy has its element as a constant, so it can splice object.[:m:] and format an int, a std::string, a double or a bool as needed.

  5. Question 5 of 9 Why can the copy for payer format a std::string while the copy for id formats an int?

    Choose one answer.

    Show the answer: Question 5 of 9

    Answer: Each copy has its member as a constant, so it splices that member with its real type

    Because the copies are made while compiling, each one uses its element's own type. That is what an ordinary loop, running when the program runs, could never do.

  6. Question 6 of 9 An enum has 400 enumerators, and string_to_enum is called for it in many files. What grows, and why?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: The compiled function and the compile time, because each enumerator gets its own copy of the comparison

    A loop over a large enum makes a longer function: every enumerator adds a copy of the comparison, compiled in each file that instantiates it. That is the cost of code that can splice each element.

  7. Question 7 of 9 Why did the second payment print "amount_rupees":49 and not 49.0?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: std::format writes the shortest text that reads back as the same double, and 49 is valid JSON for that number

    std::format("{}", 49.0) gives "49", the shortest form that converts back to exactly 49.0. JSON has a single number type, so a reader gets the same value either way.

  8. Question 8 of 9 What is the variable first for in to_json?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: It keeps a comma from being written before the first member

    The separator goes before every member except the first, so the object never starts with a comma or ends with one.

  9. Question 9 of 9 Which of these members would the flat to_json of this section get wrong or fail to compile?

    Choose every answer that is right.

    Show the answer: Question 9 of 9

    Answer:

    • A member whose type is another struct
    • A name that contains quotation marks
    • A double that is not a number

    The flat writer handles flat structs whose text contains no quotation marks. Real data breaks it in three common ways: quotation marks in text, nested structs and doubles that are not numbers.

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.

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

  1. 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.

  2. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  3. 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.

    Model answer, Pro. First step and follow-ups

  4. 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?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • enumerators_of and nonstatic_data_members_of make tables of names unnecessary: enum conversions and writers update themselves when a type changes.
  • string_to_enum returns std::optional, because text from outside may name no enumerator.
  • template for makes 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.