For years, C++ developers who needed enum-to-string conversion reached for magic_enum. The library is clever: it templates a function over enum values, extracts the string representation from __PRETTY_FUNCTION__ at compile time, and parses the enum name out of that string. It works reliably across GCC, Clang, and MSVC. The fact that it became one of the most-starred C++ header libraries on GitHub is a clear sign of how badly the language needed native reflection.
C++26 provides that natively. P2996, “Reflection for C++26”, was accepted at the November 2024 WG21 meeting in Wroclaw, authored by Wyatt Childers, Peter Dimov, Dan Katz, Barry Revzin, Andrew Sutton, Faisal Vali, and Daveed Vandevoorde. The design is built around three things: a reflect operator (^), an opaque compile-time value type (std::meta::info), and a splice operator ([: ... :]).
The Core Mechanism
The ^ operator reflects an entity into a std::meta::info value:
constexpr auto r = ^int; // reflects the type int
constexpr auto r2 = ^MyEnum::Red; // reflects an enumerator
constexpr auto r3 = ^MyStruct::x; // reflects a data member
std::meta::info is an opaque scalar type, meaningful only at compile time. You pass it to consteval functions in namespace std::meta to query properties: std::meta::name_of(r) returns the name as std::string_view, std::meta::enumerators_of(^E) returns a std::vector<std::meta::info> of all enumerators, and std::meta::nonstatic_data_members_of(^T) returns all non-static data members of a class.
The splice operator [: r :] is the generative half. Given a std::meta::info value, it inserts the named entity back into source code at that point:
using T = [: r :]; // names the reflected type
auto val = obj.[: r3 :]; // accesses the reflected member
Put it together and enum-to-string looks like this:
template <typename E> requires std::is_enum_v<E>
constexpr std::string_view enum_to_string(E value) {
template for (constexpr auto enumerator : std::meta::enumerators_of(^E)) {
if (value == [:enumerator:]) {
return std::meta::name_of(enumerator);
}
}
return "<unknown>";
}
The template for is an expansion statement from P1306, a companion proposal that provides a compile-time loop over constant ranges. The generated code is equivalent to a hand-written switch; there is no runtime overhead from the reflection machinery itself. magic_enum can be retired for new code.
The same pattern generalizes. Struct field enumeration that would have required Boost.Hana macros or Boost.PFR’s aggregate-detection tricks now reads directly:
template <typename T>
std::string to_json(const T& obj) {
std::string result = "{";
bool first = true;
template for (constexpr auto mem : std::meta::nonstatic_data_members_of(^T)) {
if (!first) result += ", ";
result += "\"";
result += std::meta::name_of(mem);
result += "\": ";
result += std::to_string(obj.[:mem:]);
first = false;
}
result += "}";
return result;
}
This works on any struct, not just aggregates, and requires no annotation or manual registration from the user.
The Flavours Framework
A recent article on isocpp.org by Bernard Teo frames C++26 reflection in terms of what kind of reflection it is, not just whether it exists. The distinction matters because “reflection” covers meaningfully different things in different languages.
Two axes define the space. The first is temporal: does reflection operate at compile time (static) or at runtime (dynamic)? The second is scope: can reflection only read the structure of types (introspection), or can it also modify or generate new entities (intercession)?
Java and C# occupy the dynamic introspection and dynamic intercession quadrants. Class.getFields(), PropertyInfo.GetValue(), Method.invoke(), dynamic proxies; all of it operates at runtime against metadata embedded in the binary. The cost is real. Reflection calls on the JVM skip JIT optimizations and prevent inlining; C# reflection allocates and is deliberately avoided in hot paths. Python sits at the far dynamic-intercession end, where metaclasses, __setattr__, and monkey-patching give full runtime control over object structure.
Rust has no native reflection. Procedural macros (proc_macro_derive) fill the gap: a macro receives a token stream representing the type definition and emits new token streams implementing derived traits. This is the mechanism behind serde::Serialize, one of the most-used crates in the ecosystem. Proc macros are compile-time and zero-cost at runtime, but they operate at the token-stream level rather than on semantic entities. The macro cannot ask the compiler what the type of a field is; it has to parse the token stream itself using crates like syn. The result is powerful but adds external build tooling and lacks type-level guarantees during generation.
C# Source Generators (Roslyn, C# 9+) are closer in spirit to C++26. They run during compilation on the syntax tree and generate new partial class content. The limitation is that they can only add to types, not inspect arbitrary types through a clean semantic API, and they require the Roslyn analyzer infrastructure.
C++26 occupies the static introspection quadrant: read-only, compile-time, integrated into the language. The ^ operator and std::meta metafunctions let you examine any type the compiler knows about at the point of instantiation, with no opt-in required and no external tooling.
Why Value-Based Rather Than Type-Based
An earlier C++ reflection proposal, reflexpr (P0194/P0385), took a different approach: each reflected entity was a unique type in a type hierarchy, composed via template metaprogramming. reflexpr(Point)::members would give you a typelist. This meant writing reflection code required the full template metaprogramming toolkit, with all its verbosity and compile-time overhead from recursive instantiations.
P2996 switched to values. std::meta::info is a scalar value type. Metafunctions return std::vector<std::meta::info>. You iterate with range-based loops or expansion statements; you store reflections in constexpr variables. The mental model is closer to normal C++ than type-level metaprogramming, and the compile-time performance is better because the compiler’s reflection API performs a direct lookup rather than driving a recursive template instantiation chain. The std::meta metafunctions are a vocabulary library over values, composable with standard algorithms. You can sort a vector of info values, filter it, pass elements as template arguments.
What C++26 Does Not Include
The reflect-and-splice design gives you compile-time code generation, but only from entities that already exist. You can enumerate the members of a struct and generate code for each one. You cannot programmatically create a new member at compile time. That capability, code injection, is the subject of P3294 (“Code Injection with Token Sequences”) and is targeted at C++29.
Herb Sutter’s metaclasses proposal (P0707) is the long-range destination: a facility to define class shapes via compile-time transformation functions, enabling patterns like $interface or $value that automatically enforce structural contracts on the annotated type. Metaclasses require both introspection (P2996, now standardized) and code injection (P3294, C++29 target). The sequencing is deliberate. Standardizing read-only introspection first gives the community time to build experience with the model before tackling the more complex question of programmatic code generation.
User-defined attribute reflection, the ability to annotate struct members with metadata that std::meta can query, is also deferred. For now, reflection can report access specifiers, constexpr, and virtual qualifiers, but not user-defined annotations.
Current State
Prototype implementations exist on two compilers. Bloomberg maintains a Clang fork for P2996 that the proposal authors use as the reference implementation. EDG, the front-end used by many EDA tools, also has a prototype. Both are accessible on Compiler Explorer under experimental compiler selections, which means you can try the syntax without building anything locally. GCC and MSVC prototypes were in earlier stages as of mid-2025.
The std::meta API surface is large, covering types, classes, enumerators, bases, members, function parameters, templates, namespaces, and aliases. The predicate library is similarly broad: is_public, is_virtual, is_constexpr, is_final, is_bit_field, and more. The size of the API reflects the scope of what the compiler has always known about C++ entities and had previously kept to itself.
What This Changes and What It Does Not
The workaround ecosystem that developed around missing native reflection, magic_enum, Boost.Hana, Boost.PFR, visit_struct, will become legacy code for new C++26 projects. Each of those libraries is a creative engineering solution to a language gap. Magic_enum’s __PRETTY_FUNCTION__ parsing trick, Boost.PFR’s aggregate decomposition via structured binding tricks, Hana’s macro-based member registration; all of them address the same underlying problem that std::meta::enumerators_of and std::meta::nonstatic_data_members_of now solve directly.
What will not change is the need for runtime reflection in use cases that genuinely require it: plugin systems that load types at runtime, ORMs mapping database results to objects without compile-time schema knowledge, scripting bridges where the type structure is unknown until the script runs. C++26 reflection does not address those cases because compile-time reflection cannot, by definition, operate on information that does not exist at compile time. Those use cases will continue to use manual registration, RTTR-style libraries, or runtime RTTI. The choice of static reflection is a genuine architectural constraint, not a limitation waiting to be fixed.
The Teo piece at isocpp.org positions C++26 as the language finally occupying the quadrant it was always suited for. That framing holds. C++ has always been a language where you pay at compile time to avoid paying at runtime. Reflection that fits that model took a long time to arrive, but when it did, the design is consistent with the language’s values rather than borrowed from a different set of trade-offs.