Reflection is one of those features C++ developers have been waiting for across multiple standard cycles, building macro-heavy workarounds in the meantime. Bernard Teo’s retrospective on isocpp.org, originally published on March 3, 2026, maps out the design space before examining what C++26 actually delivers. The thing worth unpacking further is the specific architectural choice that distinguishes P2996 from every prior C++ reflection attempt: making reflection a value-based operation rather than a type-based one.
Why Previous Attempts Were Painful
The Reflection TS (N4856) used a type-based model. reflexpr(int) produced a type, not a value, and the entire reflection API was built on type traits: reflect::get_name_v<reflexpr(T)>. Iterating over members required recursive template metaprogramming because you cannot loop over types, only recurse through them.
Boost.Hana took a different approach by encoding heterogeneous type lists as hana::tuple of hana::type_c<T> wrapper values, which moved closer to value-based thinking but still required manual struct adaptation via the BOOST_HANA_DEFINE_STRUCT macro. Without compiler support, there was no way to automatically enumerate the members of an arbitrary type. Both approaches share the same fundamental problem: reflection information is encoded as types, which means compile-time data about your types must itself be expressed through the type system. You cannot put a type in a std::vector. You cannot iterate over types with a range-for loop. Counting becomes sizeof... on a pack, filtering requires recursive partial specializations, and error messages become walls of template instantiation traces.
What P2996 Changes
P2996, authored by Barry Revzin, Wyatt Childers, Peter Dimov, Andrew Sutton, Faisal Vali, Daveed Vandevoorde, and Dan Katz, and accepted into C++26 at the Tokyo WG21 meeting in March 2024, solves this by introducing std::meta::info: a scalar, literal value type representing any C++ entity. The ^ operator produces these values:
constexpr std::meta::info r1 = ^int;
constexpr std::meta::info r2 = ^std::vector;
constexpr std::meta::info r3 = ^my_namespace;
The entire reflection API in the std::meta namespace consists of consteval functions that take and return std::meta::info values. Member enumeration returns std::vector<std::meta::info>, usable at compile time thanks to C++20’s constexpr allocations:
constexpr auto members = std::meta::nonstatic_data_members_of(^MyStruct);
constexpr auto count = members.size();
constexpr std::string_view first = std::meta::identifier_of(members[0]);
All of that is ordinary code: no template recursion, no partial specializations, no pack expansion tricks. The [: :] splice operator turns a std::meta::info back into source code:
using T = [:^int:]; // T is int
auto val = obj.[:some_reflected_member:]; // member access via reflection
std::vector<[:some_type_reflection:]> v; // template argument splice
For iterating over a range of reflections and generating code per element, C++26 introduces template for:
template for (constexpr auto member : std::meta::nonstatic_data_members_of(^T)) {
// each iteration is a separate instantiation
// but written as a normal loop with a constexpr variable per iteration
}
This is an expansion statement, not a regular loop: each iteration produces a separate instantiation, similar to fold expressions but with readable loop syntax and full access to the std::meta API inside the body.
What This Replaces
The practical payoff shows up most clearly in patterns that currently require substantial macro infrastructure. Enum-to-string conversion, which today usually means X-macros or hand-maintained tables, becomes a short generic function:
template <typename E>
requires std::is_enum_v<E>
constexpr std::string_view enum_to_string(E value) {
template for (constexpr auto e : std::meta::enumerators_of(^E)) {
if (value == [:e:])
return std::meta::identifier_of(e);
}
return "<unknown>";
}
Struct serialization follows the same pattern. Iterating nonstatic_data_members_of, using identifier_of for field names, and splicing the member pointer for access reduces what would normally require a code generator or macro-heavy adaptation to a small generic function. Bidirectional enum conversion, structural binding for arbitrary aggregates, ORM-style struct-to-column mapping, and command-line flag generation from struct fields all collapse from multi-hundred-line macro systems to concise templates.
In systems programming contexts this has direct implications. Qt’s moc and Unreal Engine’s UPROPERTY/UCLASS system both exist because the language never provided this introspection natively. The Qt meta-object compiler runs as a separate preprocessing step, generating C++ source files that are then compiled alongside your code. Unreal’s reflection annotations require a source-level pass by UnrealHeaderTool. With P2996, both become candidates for replacement with ordinary template code, removing an entire build pipeline stage. Rust’s clap derive does something similar for CLI argument parsing via proc macros; C++26 enables the same pattern without a separate code generation step.
How It Compares to Other Languages
Rust’s proc macros receive and emit token streams. Libraries like syn and quote provide a practical AST layer, but proc macros operate on syntax, not semantics. You see the textual type annotation of a field, not its fully-resolved type. C++26 reflection operates after name lookup and type checking: std::meta::type_of on a reflected member returns a reflection of its resolved type, not a string you then have to parse. Proc macros can generate arbitrary syntax; C++26 splicing is constrained to well-formed AST nodes. The tradeoff favors C++26 for anything requiring type-system integration, and proc macros for anything requiring truly open-ended code synthesis.
D’s __traits provides compile-time introspection through a built-in keyword: __traits(allMembers, T) returns a tuple of member name strings, and __traits(getMember, obj, name) retrieves a member by name. It works, but returning strings rather than structured values limits composability. You cannot call a single unified API on a D __traits result; you call separate __traits queries for protection level, type, and so on. D also has mixin for compile-time code synthesis from strings, which is flexible but not type-safe at the generation point. C++26’s API is more composable because all information flows through std::meta::info, a single value type with a uniform query interface.
Java and C# reflection are runtime operations with associated overhead and no compile-time guarantees. C#‘s source generators, introduced in Roslyn, are the closest analogue in intent: they run at compile time and generate code that ships with the assembly, operating on semantic models via Roslyn APIs. But they output text files that are re-compiled, living outside the type system rather than integrated with it. The System.Text.Json source generator is a practical application of this pattern, essentially doing manually in Roslyn what C++26 enables intrinsically. C++26 reflection is zero-overhead by construction: all queries are consteval, all results are computed at compile time, and generated code is identical to hand-written code.
Implementation Status
The best way to experiment with P2996 today is through Compiler Explorer, which provides an EDG-based build with reflection support. EDG has had the most complete implementation since mid-2023 and tracks paper revisions closely; it is what the P2996 authors use for their own testing.
Bloomberg maintains a Clang fork implementing P2996 with the core ^ operator, most std::meta functions, and template for substantially complete. Work to upstream this into LLVM mainline was ongoing through 2024 and into 2025, with partial support landing in LLVM trunk under experimental flags. GCC has a community-driven implementation that lagged behind the Clang and EDG work. MSVC had not announced a public implementation as of this writing, though Microsoft participates in WG21.
The Compile-Time Cost
template for generates one instantiation per element, linear in the number of members, similar in cost to fold expressions. For large structs, this matters: a type with a hundred members produces a hundred instantiation steps. The key advantage over Boost.Hana is that reflection queries do not create intermediate types. std::meta::info is a single type regardless of what it represents; both the EDG and Clang implementations memoize per-type reflection queries. Early data from the P2996 authors suggests enum-to-string via reflection compiles comparably to optimized X-macro techniques, and considerably faster than recursive template approaches.
The constexpr std::vector<std::meta::info> allocations run through the compiler’s constant-expression heap, which adds implementation complexity but is supported by both EDG and the Bloomberg Clang fork. The performance story is meaningfully better than Boost.Hana for large types because the template instantiation count stays low: every reflection query returns the same std::meta::info type, so the compiler’s instantiation machinery sees far fewer distinct types.
What Lands in C++26
P2996 covers introspection of existing entities. The companion paper P3068 extends this with std::meta::define_aggregate, allowing new struct types to be synthesized at compile time from a list of reflected members. This is where struct-of-arrays transformations, proxy type generation, and layout-sensitive code patterns become straightforward. Related papers handle function parameter reflection, member offset queries for bitfield-aware layout work, and expansion statements.
The combination gives C++ something it has not had at the language level: a way to write code that treats the structure of types as data to be computed over, without macros, without external code generators, and without runtime overhead. The value-based model is the reason this works where previous attempts did not. By making std::meta::info a value rather than a type, every normal tool the language provides for manipulating values at compile time, including containers, algorithms, consteval functions, and ordinary control flow, becomes available for reflection work. After decades of workarounds, that is a meaningful change to what the language can express.