· 6 min read ·

C++26 Static Reflection: The Value-Based Design After Two Decades of Debate

Source: isocpp

Reflection is one of those features that C++ has needed for a long time. The capability, in some form, has been circulating through WG21 proposals since at least 2007. C++26 will finally ship it, and as Bernard Teo’s writeup on isocpp.org notes (originally published March 3, 2026), implementation work in Clang and GCC is already substantial enough that this is no longer theoretical. The more interesting story, though, is why the final design looks the way it does and what that means for how you will actually use it.

The Long Detour Through Type-Based Reflection

The earliest serious standardization attempt was P0385 from Matúš Chochlík, Axel Naumann, and David Sankel, which proposed a reflexpr() keyword that returned a type:

// Type-based approach (P0385, not adopted)
using refl = reflexpr(int);
using name = std::reflect::get_name_v<refl>;  // trait query

This fits neatly into the template metaprogramming tradition. Types, trait queries, specializations, recursive template instantiation. C++ developers of a certain background find this completely natural. The problem is that the type-based approach forces you to express what could be ordinary control flow as recursive template instantiation. Filtering a list of member reflections means writing a recursive template specialization. Iterating over reflected members means unrolling a parameter pack. Error messages from compilers become walls of template noise that are often harder to read than the problem they describe.

Herb Sutter’s metaclasses proposal (P0707) from 2017 attacked the problem from a different angle, proposing compile-time transformation of class definitions through a $class construct. The ideas were compelling but too radical to standardize. The reflection portions were eventually split out and continued separately.

The Value-Based Decision That Changed the Trajectory

The turning point came with P1240 in 2018, authored by Daveed Vandevoorde, Wyatt Childers, and Andrew Sutton. They introduced the central idea that reflection values should be exactly that: values. Not types, not template parameters wrapped in types, but first-class scalar values of an opaque type called std::meta::info.

The proposal that became C++26 reflection, P2996, builds directly on this foundation. The ^ operator reflects any C++ entity into a std::meta::info value. The [: :] splice operator inserts a reflected entity back into source code. Both operate exclusively at compile time, in consteval and constexpr contexts.

#include <meta>

// Reflect a type into a value
constexpr std::meta::info r = ^int;

// Query it with ordinary function calls
static_assert(std::meta::name_of(r) == "int");

// Splice it back into code
using T = [:r:];  // T is int

The ergonomic difference from type-based approaches is significant. Because std::meta::info is a regular value, you can store it in a std::vector, filter it with std::ranges::filter, iterate over collections with ordinary for loops, and pass it to consteval functions that look like normal C++ code:

template<typename T>
consteval void inspect_members() {
    for (auto m : std::meta::members_of(^T)) {
        if (std::meta::is_nonstatic_data_member(m)) {
            auto name = std::meta::name_of(m);
            auto type = std::meta::type_of(m);
            // use name, type...
        }
    }
}

Compare that to the equivalent P0385 code, which would require a recursive template or a fold expression over a reflected type list. The P2996 version is something a C++ programmer with no TMP background can read on the first pass. That accessibility matters for adoption.

What the Splice Operator Unlocks

The [: :] splice syntax is where the feature becomes genuinely powerful. You can splice into type positions, expression positions, template argument lists, and member access expressions:

struct Point { int x; int y; };
constexpr std::meta::info mx = ^Point::x;

Point p{3, 7};
int val = p.[:mx:];  // equivalent to p.x, val == 3

This enables zero-boilerplate serialization code that previously required either macros or separate registration steps. Libraries like Boost.Describe and Boost.PFR already solve portions of this problem through macro-based registration and aggregate inspection tricks respectively. P2996 makes both approaches unnecessary for new code without requiring any opt-in:

template<typename T>
std::string to_json(const T& obj) {
    std::string out = "{";
    bool first = true;
    template for (constexpr auto m : std::meta::members_of(^T)) {
        if constexpr (std::meta::is_nonstatic_data_member(m)) {
            if (!first) out += ", ";
            out += std::string(std::meta::name_of(m)) + ": ";
            out += std::to_string(obj.[:m:]);
            first = false;
        }
    }
    return out + "}";
}

The enum-to-string problem, currently addressed by libraries like magic_enum through compiler-specific __PRETTY_FUNCTION__ hacks, also becomes clean:

template<typename E>
    requires std::meta::is_enum(^E)
constexpr std::string_view enum_to_string(E value) {
    template for (constexpr auto e : std::meta::enumerators_of(^E)) {
        if ([:e:] == value)
            return std::meta::name_of(e);
    }
    return "<unknown>";
}

How This Compares to Other Languages

Runtime reflection in Java and C# is fundamentally different in character. Class.forName(), Method.invoke(), and the System.Reflection namespace all operate at runtime, which means they can load types from dynamically linked code, support plugin systems, and power ORMs that generate queries from class metadata. They pay for this with boxing, virtual dispatch overhead, and weak static typing. C++26 reflection does none of those things, but the tradeoff is intentional: std::meta::info values cannot escape to runtime at all. For a systems language where zero-overhead is a founding principle, that constraint is a feature.

Rust does not have built-in reflection. The closest analog is procedural macros, which are compile-time programs that receive a token stream and emit a modified token stream. They accomplish many of the same goals for serialization, particularly through #[derive(Serialize, Deserialize)] from the serde crate. The key difference is that proc-macros operate on tokens before type-checking, not on semantic type information. They require a separate compilation step, pull in heavy dependencies like syn and quote, and produce error messages that can be difficult to trace back to the original code. C++26 reflection operates on fully resolved semantic entities, which gives better diagnostics and allows querying information that proc-macros simply cannot access.

D’s compile-time introspection through __traits and std.traits is probably the closest existing parallel, and it has been working well in production for years. D uses string-based member name access and mixin for code injection rather than an opaque value type and splice operator, but the practical use cases look similar. The C++26 design is more composable because std::meta::info collections work with standard algorithms.

Implementation Status and When You Can Use It

The reference implementation lives in a Clang fork maintained by Wyatt Childers and colleagues, originally hosted under the Bloomberg organization on GitHub. You can experiment with it right now on Compiler Explorer by selecting the experimental P2996 Clang builds. The EDG frontend, which underlies MSVC, has implementation work from Daveed Vandevoorde since he is a P2996 co-author. GCC work is progressing but has lagged the Clang fork.

The C++26 standard is due for publication in 2026. Compiler vendors typically start shipping features before the formal standard drops, and given that the primary authors are directly associated with the major compiler frontends, early support seems likely. The existing fork already handles the core feature set: member enumeration, splice expressions, the std::meta query library, and the template for expansion statement.

The Implications for C++ Idioms

One consequence worth dwelling on: P2996 replaces a large category of template metaprogramming that currently requires TMP expertise to write and to maintain. Type lists, recursive instantiation patterns, and SFINAE-based trait detection become unnecessary for most of their current uses. std::tuple manipulation, type filtering, and index sequences are all cleaner with value-based reflection, which lowers the barrier for writing generic code without lowering the ceiling for what expert users can accomplish.

Qt’s meta-object compiler, which processes C++ source files to generate reflection metadata for signals and slots, is frequently cited as a practical motivation. With C++26 reflection, moc becomes unnecessary for new Qt-style property systems, and that is an immediate, tangible benefit for one of the most widely used C++ frameworks in existence.

The design took nearly twenty years to converge. Competing proposals argued over type-based versus value-based designs, over how much code synthesis to allow, over access control semantics for private member reflection, and over how reflection should interact with C++20 modules. The value-based approach that P2996 settles on is not the most obvious choice if you come from the TMP tradition, but it is the most ergonomic and the most likely to be used correctly across the range of C++ programmers. Getting compile-time introspection that reads like ordinary code, carries zero runtime overhead, and integrates with the standard library without a separate registration step is what makes this worth the wait.

Was this interesting?