ESL Engineering Software Lab
ESL Engineering Software LabSoftware Engineering Consultancy
C++26 Reflection · GCC 16.1 Run on Compiler Explorer Contact Us
Built and run on GCC 16.1, 29 September 2026

C++26 Reflection on GCC 16.1:
What Actually Compiles Today

Amadeus Ferro's print_fields(), plus a second job for the same feature, built and run on the shipping compiler. The six things that stop the snippet compiling on the first try, where the rest of the toolchain still lags, and one consequence for teams under a coding standard.

Daniel Liezrowice, ESL Engineering Software Lab · eswlab.com/contact-us

Context

The post shows one function. We ran it, extended it, and broke it on purpose.

What the post shows

print_fields() works on any struct. Add a field, the function picks it up. Four pieces: ^^T, nonstatic_data_members_of, template for, [:m:]. No macro, no codegen, no registration list. Everything disappears before main().

Original post by Amadeus Ferro: lnkd.in/p/dndmzJYN

What this deck adds

  • Which compilers actually have it today (one of three).
  • The same program written the old way and the reflection way, both runnable in Compiler Explorer.
  • A second job for reflection: a compile-time wire-layout check that replaces five offsetof asserts.
  • Two negative tests that prove "add a field and it just works" instead of asserting it.
  • Six compile errors you will hit porting the snippet to GCC 16.1, with the fix for each.
  • Where the toolchain around the compiler still cannot read this code.
Verified status, September 2026

"Just shipped" means GCC. Only GCC.

Standard

P2996R13 Reflection for C++26 is in the frozen C++26 draft (Croydon, March 2026). template for is a separate paper, P1306 Expansion statements, adopted alongside it. Both are needed for the snippet in the post.

GCC 16.1

Released 30 April 2026. Enabled with -std=c++26 -freflection, header <meta>. GCC's own release notes call it experimental. Also ships annotations (P3394), define_static_* (P3491) and contracts.

Clang and MSVC

Mainline Clang: nothing. Only the Bloomberg clang-p2996 fork, with header <experimental/meta>. MSVC: no public implementation and no published ETA. If you ship on Visual Studio this is "prepare", not "deploy".

Everything in this deck was compiled and executed on GCC 16.1 through Compiler Explorer (compiler id g161). Links on the code slides.

pre_cpp26/fault_telemetry.cpp: three jobs, three workarounds

#define FAULT_LIST(X)   \                                    // job 1: X-macro so the enum and its names stay in sync
    X(None) X(OverTemp) X(UnderVoltage) X(SensorTimeout)

enum class Fault : std::uint8_t {
#define AS_ENUM(name) name,
    FAULT_LIST(AS_ENUM)
#undef AS_ENUM
};

constexpr const char* to_string(Fault f) {
    switch (f) {
#define AS_CASE(name) case Fault::name: return #name;         // stringize with #
        FAULT_LIST(AS_CASE)
#undef AS_CASE
    }
    return "<unknown>";
}

struct TelemetryFrame {
    std::uint32_t timestamp_ms;  float temp_c;  std::uint16_t voltage_mv;
    std::uint8_t  fault;         std::uint8_t crc8;
};
static_assert(offsetof(TelemetryFrame, timestamp_ms) == 0);      // job 2: wire layout, one line per field,
static_assert(offsetof(TelemetryFrame, temp_c)       == 4);      //        edited by hand when the struct changes
static_assert(offsetof(TelemetryFrame, voltage_mv)   == 8);
static_assert(offsetof(TelemetryFrame, fault)        == 10);
static_assert(offsetof(TelemetryFrame, crc8)         == 11);

void print_frame(const TelemetryFrame& f) {              // job 3: the struct, listed again by hand
    std::printf("timestamp_ms = %u\n", f.timestamp_ms);
    std::printf("temp_c       = %g\n", static_cast<double>(f.temp_c));
    std::printf("voltage_mv   = %u\n", f.voltage_mv);
    std::printf("fault        = %s\n", to_string(static_cast<Fault>(f.fault)));
    std::printf("crc8         = %u\n", f.crc8);
    // Add a field to TelemetryFrame and forget this function: still compiles.
}

cpp26/fault_telemetry.cpp: same three jobs, one definition of everything

#include <meta>
enum class Fault : std::uint8_t { None, OverTemp, UnderVoltage, SensorTimeout };

template <typename E> requires std::is_enum_v<E>
constexpr std::string_view enum_name(E value) {                   // job 1: names come from the compiler
    template for (constexpr auto e : std::define_static_array(std::meta::enumerators_of(^^E))) {
        if (value == [:e:]) return std::meta::identifier_of(e);
    }
    return "<unknown>";
}

struct TelemetryFrame {
    std::uint32_t timestamp_ms;  float temp_c;  std::uint16_t voltage_mv;
    Fault fault;                 std::uint8_t crc8;
};

template <typename T>
consteval bool is_padding_free() {                                 // job 2: one loop replaces 5 x offsetof
    std::size_t expected = 0;
    for (auto m : std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::current())) {
        if (static_cast<std::size_t>(std::meta::offset_of(m).bytes) != expected) return false;
        expected += std::meta::size_of(m);
    }
    return expected == sizeof(T);
}
static_assert(is_padding_free<TelemetryFrame>(), "TelemetryFrame has padding: wire format broken");

template <typename T>
void print_fields(const T& obj) {                                  // job 3: the function from the post
    template for (constexpr auto m : std::define_static_array(
                      std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::current()))) {
        if constexpr (std::is_enum_v<typename[:std::meta::type_of(m):]>)
            std::println("{:<12} = {}", std::meta::identifier_of(m), enum_name(obj.[:m:]));
        else
            std::println("{:<12} = {}", std::meta::identifier_of(m), obj.[:m:]);
    }
}
Executed on GCC 16.1, Compiler Explorer, 29 Sep 2026

Same output. Then two edits the old code could not catch.

Clean build, identical stdout

timestamp_ms = 1000 temp_c = 71.5 voltage_mv = 3300 fault = OverTemp crc8 = 90

No warnings with -Wall -Wextra -Wpedantic. Byte-for-byte equal to the C++20 binary's output.

Edit 1: insert a uint8_t before timestamp_ms

error: static assertion failed: TelemetryFrame has padding: wire format broken

Build fails. The pre-C++26 version needs someone to remember to update five offsetof lines.

Edit 2: add seq_no field and WatchdogReset enumerator, nothing else

fault = WatchdogReset crc8 = 90 seq_no = 42

Both appear. The pre-C++26 print_frame would still compile and print a stale frame.

Six errors between the post's snippet and a running binary · each one reproduced on GCC 16.1

What it took to make it compile

You writeGCC 16.1 saysFix
^E (single caret, as in early P2996 drafts)expected primary-expression before '^' tokenThe reflection operator is ^^ since P2996R4; ^E is not valid.
template for (constexpr auto e : enumerators_of(^^E))not a constant expression because it refers to a result of 'operator new'enumerators_of returns a std::vector. Wrap it: std::define_static_array(...) (P3491) gives a constant-usable span.
nonstatic_data_members_of(^^T)too few arguments to function ... (info, access_context)P2996R13 added the second parameter. Pass std::meta::access_context::current().
offset_of(m).bytes != expectedwarning: comparison of integer expressions of different signedness [-Wsign-compare].bytes is ptrdiff_t, not size_t. Cast, or make the accumulator signed. Compiles either way.
#include <experimental/meta>fatal error: experimental/meta: No such file or directoryThat header belongs to the Bloomberg clang fork. GCC 16 ships <meta>.
-std=c++26 alone, or -std=c++23 -freflectionreflection is only available with '-freflection' / '-freflection' only supported with '-std=c++26' or '-std=gnu++26'Both flags, together: -std=c++26 -freflection.

None of these are in the post's snippet, which is written against the paper rather than the compiler. All six were reproduced through the Compiler Explorer API on 29 September 2026; the script and raw output are in the reproduction folder linked on the last slide.

The compiler is ahead of everything that reads the compiler's input

GCC parses this. Check whether your other tools do.

What we tested

We fed cpp26/fault_telemetry.cpp to a commercial C++ static analyzer with an EDG-based front-end (Parasoft C/C++test, both the 2025.2 and the May 2026 release). It reported a clean scan, exit code 0, no parse error.

  • It flagged Fault (line 12) and print_fields (line 52) for a global-namespace rule, but not TelemetryFrame (line 26), a plain struct sitting between them. The same rule flags TelemetryFrame in the C++20 file.
  • enum_name and is_padding_free were not seen at all. Flow analysis built one function graph (main); the C++20 file produced three.
  • A control file with a deliberate syntax error also printed no parse error: recovery is silent by default.

Why it matters for a C++ team

Every tool that has its own parser is a separate port: static analyzers, IDE indexers, formatters, coverage instrumenters, documentation generators. ^^, [: :] and template for are new syntax, not new library, so each parser has to be taught them.

Until then a green result from such a tool on reflection code means "I skipped it", not "it is fine". Test with a known-bad file first, and compare function counts between the compiler and the tool.

GCC itself labels the feature experimental. Treat the ecosystem the same way.

One consequence the post does not mention

For teams under MISRA C++:2023, the boilerplate reflection removes is the boilerplate the standard forbids

JobPre-reflection idiomMISRA C++:2023Reflection replacement
Enum to stringX-macro list plus #name19.0.2 Required no function-like macros
19.3.1 Advisory no # / ##
enumerators_of(^^E) + identifier_of(e)
Wire-format layoutstatic_assert(offsetof(Frame, f) == N) per field21.2.4 Required offsetof shall not be usedoffset_of(m) / size_of(m) in one consteval loop
Field dumpStruct listed a second time by handNo rule; silent rot insteadtemplate for over nonstatic_data_members_of

Measured on the old code

The C++20 file yields 29 MISRA C++:2023 findings in Parasoft C/C++test (identical on 2025.2 and 2026.1), 14 of them caused directly by the macro, #, offsetof and switch idioms above, and 8 more by printf.

By construction on the new code

The C++26 file contains no macro, no #, no offsetof, no switch, no printf. Those rules cannot fire on tokens that are not there. That is a stronger statement than any scan result, because the scanner has not parsed the code yet (previous slide). MISRA has no reflection rules of its own so far.

What to do this quarter

Recommendations by where you stand

GCC shops

  • Pilot -std=c++26 -freflection on non-shipping code: enum names, layout asserts, test-only field dumps.
  • Keep the C++20 fallback until GCC drops "experimental".
  • Gate with negative tests like slide 6, not just "it compiles".

Clang and MSVC shops

  • Nothing to deploy yet. Isolate today's X-macros and offsetof tables behind one header so the swap is mechanical later.
  • Try the Bloomberg clang-p2996 fork for experiments only; its header and API lag the final paper.

Whoever owns the toolchain

  • Run a known-bad reflection file through every parser you depend on and see who complains. Silence is the bad answer.
  • Ask analyzer and IDE vendors for their P2996 front-end date in writing.
  • If you are under MISRA, plan the deviation records for 19.0.2 / 19.3.1 / 21.2.4 with "pending C++26 reflection" as the closure.
ESL Engineering Software Lab

Modern C++ migration, static analysis and safety evidence for embedded C and C++

We run this kind of before/after measurement on client codebases: which modernisation steps pay off, which ones only move the problem, and where the toolchain is not ready yet.

Contact Ushttps://eswlab.com/contact-us/

Reproduce everything

  • C++26 version on GCC 16.1: https://godbolt.org/z/M7os6nq9c
  • Pre-C++26 version on GCC 15.2: https://godbolt.org/z/jaTbPzEM7
  • Original post: https://lnkd.in/p/dndmzJYN
  • Gotcha reproductions: each variant on slide 7 was sent to g161 through the Compiler Explorer API with -std=c++26 -freflection -O2 -Wall -Wextra; the quoted messages are GCC's verbatim output.
  • Analyzer test: Parasoft C/C++test 2025.2 and 2026.1, builtin://MISRA C++ 2023, compiler profile gcc_11-64, 29 September 2026.
MISRA and MISRA C++ are registered trademarks of The MISRA Consortium Limited. Parasoft and C/C++test are trademarks of Parasoft Corporation. ESL is an independent consultancy.
1 / 11
← Swipe to navigate →