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
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
offsetof asserts.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.
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.
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.
#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. }
#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:]); } }
No warnings with -Wall -Wextra -Wpedantic. Byte-for-byte equal to the C++20 binary's output.
uint8_t before timestamp_msBuild fails. The pre-C++26 version needs someone to remember to update five offsetof lines.
seq_no field and WatchdogReset enumerator, nothing elseBoth appear. The pre-C++26 print_frame would still compile and print a stale frame.
| You write | GCC 16.1 says | Fix |
|---|---|---|
^E (single caret, as in early P2996 drafts) | expected primary-expression before '^' token | The 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 != expected | warning: 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 directory | That header belongs to the Bloomberg clang fork. GCC 16 ships <meta>. |
-std=c++26 alone, or -std=c++23 -freflection | reflection 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.
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.
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.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.
| Job | Pre-reflection idiom | MISRA C++:2023 | Reflection replacement |
|---|---|---|---|
| Enum to string | X-macro list plus #name | 19.0.2 Required no function-like macros 19.3.1 Advisory no # / ## | enumerators_of(^^E) + identifier_of(e) |
| Wire-format layout | static_assert(offsetof(Frame, f) == N) per field | 21.2.4 Required offsetof shall not be used | offset_of(m) / size_of(m) in one consteval loop |
| Field dump | Struct listed a second time by hand | No rule; silent rot instead | template for over nonstatic_data_members_of |
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.
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.
-std=c++26 -freflection on non-shipping code: enum names, layout asserts, test-only field dumps.offsetof tables behind one header so the swap is mechanical later.clang-p2996 fork for experiments only; its header and API lag the final paper.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/
g161 through the Compiler Explorer API with -std=c++26 -freflection -O2 -Wall -Wextra; the quoted messages are GCC's verbatim output.builtin://MISRA C++ 2023, compiler profile gcc_11-64, 29 September 2026.