Skip to content
C++
Language
since C++26
Expert

Static Reflection (C++26)

P2996 static reflection: query type members and metadata at compile time via std::meta — enabling auto-serialization and zero-cost introspection without macros.

Static Reflection (P2996)since C++26

Static reflection exposes type, member, and declaration metadata as compile-time values (std::meta::info) that can be examined with consteval functions in <meta> — allowing code generation from struct definitions with zero runtime overhead and no macros.

Overview

Before C++26, compile-time introspection required painful workarounds: X-Macro patterns, repeated explicit field lists, or external code generators. P2996 standardizes the ^^ (reflection) and [: :] (splice) operators plus a std::meta API surface to make this first-class.

Key concepts:

ConceptMeaning
^^TReflect on type T — produces a std::meta::info
^^exprReflect on an expression or declaration
[:r:]Splice — turn a std::meta::info back into a usable entity
std::meta::members_of(r)Compile-time range of a type's members
std::meta::name_of(r)String name of the reflected entity

Basic Usage

cpp
#include <meta>
#include <print>
#include <string_view>

struct Point { int x; int y; float z; };

consteval std::string_view field_name(std::meta::info m) {
    return std::meta::name_of(m);
}

int main() {
    // List all non-static data members  // C++26
    constexpr auto members = std::meta::nonstatic_data_members_of(^^Point);
    // members is a constexpr range of std::meta::info

    // Iterate at compile time (via template expansion)
    [:members...;] // expansion statement (P1306)
}

Auto-Serialization

The killer use case: derive to_json() from a struct definition automatically.

cpp
#include <meta>
#include <string>
#include <format>

template<typename T>
std::string to_json(const T& obj) {
    std::string result = "{";
    bool first = true;
    template for (constexpr auto member : std::meta::nonstatic_data_members_of(^^T)) {
        if (!first) result += ", ";
        first = false;
        result += std::format("\"{}\":{}", 
            std::meta::name_of(member),
            obj.[:member:]  // splice: access the actual field
        );
    }
    return result + "}";
}

struct Config { int port; std::string host; bool tls; };

// Usage
Config cfg{8080, "localhost", true};
// to_json(cfg) → {"port":8080, "host":"localhost", "tls":true}

No macros. No explicit field registration. The loop expands at compile time.

Querying Type Metadata

cpp
#include <meta>

// Check if a type has a specific member
consteval bool has_member(std::meta::info type_info, std::string_view name) {
    for (auto m : std::meta::nonstatic_data_members_of(type_info)) {
        if (std::meta::name_of(m) == name) return true;
    }
    return false;
}

struct Foo { int x; };
struct Bar { float y; };

static_assert(has_member(^^Foo, "x"));
static_assert(!has_member(^^Bar, "x"));

Enum-to-String (Without Macros)

cpp
#include <meta>
#include <string_view>
#include <optional>

enum class Color { Red, Green, Blue };

constexpr std::optional<std::string_view> enum_name(Color c) {
    template for (constexpr auto e : std::meta::enumerators_of(^^Color)) {
        if ([:e:] == c) return std::meta::name_of(e);
    }
    return std::nullopt;
}

// enum_name(Color::Green) == "Green"   (constexpr, zero overhead)

Compare with the pre-C++26 alternatives: magic_enum (compiler-limit hacks), X-macros (fragile), manual switch (unmaintainable at scale).

Splice Operator [: :]

Splicing converts a std::meta::info back into a language construct:

cpp
struct Vec3 { float x, y, z; };

// Access field by reflected info
constexpr auto x_info = ^^Vec3::x;
Vec3 v{1.f, 2.f, 3.f};
float val = v.[:x_info:];   // same as v.x — resolved at compile time

// Splice a type
using T = [:std::meta::type_of(x_info):];   // T == float

// Splice in template arguments
template<typename T> struct Box { T value; };
constexpr auto meta_float = ^^float;
Box<[:meta_float:]> b{3.14f};   // Box<float>

Generating Overload Sets

cpp
#include <meta>

// Generate a visit function for a variant from its types
template<typename Variant>
constexpr auto make_visitor(auto&&... lambdas) {
    struct Visitor : decltype(lambdas)... {
        using decltype(lambdas)::operator()...;
    };
    return Visitor{std::forward<decltype(lambdas)>(lambdas)...};
}

Reflection vs Template Metaprogramming

FeatureTMP (pre-C++26)Reflection (C++26)
Access member namesImpossible without macrosstd::meta::name_of(m)
Iterate struct fieldsX-Macro or manualnonstatic_data_members_of
Count enum valuesMagic-enum hacksstd::meta::enumerators_of
Access field typesRequires explicit traitsstd::meta::type_of(m)
ReadabilityTemplate soupconsteval functions

The ^^ Operator vs XOR

C++ already uses ^ as the bitwise XOR operator, so using a single caret for reflection would create an ambiguity in certain parse contexts. P2996 originally proposed ^ but P3381R0 adopted ^^ (double caret) to make every reflection expression unambiguous — the parser never needs to look ahead to decide whether a ^ is XOR or a reflector.

cpp
// C++26
int a = 5, b = 3;
int xor_result  = a ^ b;          // still XOR — single caret, two operands
auto r          = ^^int;          // reflection — double caret, one operand (a type)
auto r2         = ^^MyStruct;     // reflect a class type
auto r3         = ^^::;           // reflect the global namespace
auto r4         = ^^std;          // reflect the std namespace

// Both work together in the same expression without confusion:
bool same = (^^int == ^^int);     // true — info values are comparable

The rule is simple: ^^ always introduces a reflection; single ^ between two expressions is always XOR. The global-namespace reflection ^^:: is particularly useful when iterating top-level declarations programmatically.

Base Class Iteration

std::meta::base_classes_of returns a compile-time range of std::meta::info values, one per direct base. Combined with std::meta::type_of you can recover the actual base type, and std::meta::is_public lets you filter access specifiers.

cpp
#include <meta>
#include <print>

struct Animal  { virtual ~Animal() = default; };
struct Flyable { void fly(); };
struct Bird : public Animal, public Flyable { int feathers; };

consteval void describe_bases() {
    // C++26
    template for (constexpr auto b : std::meta::base_classes_of(^^Bird)) {
        // Recover the base type as a usable type via splice
        using BaseType = [:std::meta::type_of(b):];

        std::print("base: {}  public: {}\n",
            std::meta::identifier_of(b),
            std::meta::is_public(b));
    }
}
// Output (compile-time):
//   base: Animal   public: true
//   base: Flyable  public: true

For virtual or private bases the same API applies — std::meta::is_virtual(b) and std::meta::is_private(b) give you the access and virtuality flags. This makes it straightforward to generate serialization or visitor code that respects the full inheritance chain without a single typeid or dynamic_cast at runtime.

Function Reflection

P2996 lets you reflect on functions just as easily as types. std::meta::parameters_of(^^foo) returns a range over the parameter reflections, each carrying its name and type. std::meta::return_type_of(^^foo) gives the return type as a std::meta::info.

cpp
#include <meta>
#include <string>
#include <format>

int add(int lhs, int rhs);         // declaration is enough

// Build a compile-time signature string from reflection
consteval std::string signature_of(std::meta::info fn) {
    // C++26
    std::string sig;
    sig += std::meta::identifier_of(std::meta::return_type_of(fn));
    sig += " ";
    sig += std::meta::identifier_of(fn);
    sig += "(";
    bool first = true;
    for (auto p : std::meta::parameters_of(fn)) {
        if (!first) sig += ", ";
        first = false;
        sig += std::meta::identifier_of(std::meta::type_of(p));
        sig += " ";
        sig += std::meta::identifier_of(p);
    }
    sig += ")";
    return sig;
}

static_assert(signature_of(^^add) == "int add(int lhs, int rhs)");

The same technique scales to generating dispatch wrappers, RPC stubs, or documentation strings entirely at compile time. Because parameters_of gives you both the parameter name and its type reflection, you can splice those back into generated code without any string parsing.

define_aggregate — Generating New Types

std::meta::define_aggregate is the most experimental part of P2996. It lets you programmatically construct a new struct type at compile time from a list of std::meta::data_member_spec descriptors. The result is a fully usable C++ type you can use in templates, store values of, and reflect on again.

cpp
#include <meta>
#include <vector>
#include <string_view>

// C++26 — experimental; subject to change before final standardization
//
// Given a list of (name, type-reflection) pairs, produce a struct with those fields.
// Example use case: a "parallel struct" that mirrors another struct's layout.

consteval std::meta::info make_parallel_struct(
    std::span<const std::pair<std::string_view, std::meta::info>> fields)
{
    std::vector<std::meta::data_member_spec> specs;
    specs.reserve(fields.size());
    for (auto [name, type_r] : fields) {
        specs.push_back(std::meta::data_member_spec{
            .name = name,
            .type = type_r,
        });
    }
    // define_aggregate returns the std::meta::info for the newly created type
    return std::meta::define_aggregate(specs);
}

// Build a struct equivalent to: struct MyPoint { float x; float y; };
constexpr std::pair<std::string_view, std::meta::info> point_fields[] = {
    {"x", ^^float},
    {"y", ^^float},
};
constexpr auto MyPoint_r = make_parallel_struct(point_fields);
using MyPoint = [:MyPoint_r:];   // splice it into a real type alias

MyPoint p;
p.x = 1.0f;
p.y = 2.0f;

Best practice. Use define_aggregate only when the set of fields is genuinely dynamic at compile time (e.g., derived from another reflected type). For most code-gen needs, template for over nonstatic_data_members_of is simpler and more readable. define_aggregate is likely to see API tweaks before C++26 ships in implementations.

Consteval Validation Using Reflection

Reflection pairs naturally with static_assert to enforce structural invariants at compile time, turning what would otherwise be a confusing linker or runtime error into a clear diagnostic at the static_assert site.

cpp
#include <meta>
#include <string>
#include <format>

// C++26 — verify no two enumerators share the same value
template<typename E>
    requires std::is_enum_v<E>
consteval void assert_no_duplicate_enum_values() {
    constexpr auto enumerators = std::meta::enumerators_of(^^E);
    for (std::size_t i = 0; i < enumerators.size(); ++i) {
        for (std::size_t j = i + 1; j < enumerators.size(); ++j) {
            if ([:enumerators[i]:] == static_cast<E>([:enumerators[j]:])) {
                // Provide the names in the error message via identifier_of
                // (consteval context — this will be a compile error, not UB)
                throw std::logic_error(
                    std::format("duplicate values: {} and {}",
                        std::meta::identifier_of(enumerators[i]),
                        std::meta::identifier_of(enumerators[j])));
            }
        }
    }
}

// Verify that every non-static data member of a struct has a non-void type
template<typename T>
consteval void assert_no_void_members() {
    // C++26
    template for (constexpr auto m : std::meta::nonstatic_data_members_of(^^T)) {
        static_assert(
            !std::meta::is_void_type(std::meta::type_of(m)),
            "struct member has void type — reflection found a problem"
        );
    }
}

enum class Status { Ok = 0, Warn = 1, Error = 2 };
struct Response { int code; std::string body; };

// These fire at compile time if the invariants are violated:
constexpr auto _check_status  = (assert_no_duplicate_enum_values<Status>(), 0);
constexpr auto _check_response = (assert_no_void_members<Response>(), 0);

Best practice. Placing these validations in a constexpr variable initializer (as shown above) ensures they run at compile time even when the function is never called explicitly. This is the reflection-era replacement for hand-written static_assert chains.

Complete std::meta API Table

The table below covers the key query functions from the P2996 specification. All functions live in namespace std::meta and operate on std::meta::info values produced by ^^.

FunctionSignature (simplified)ReturnsStandard
enumerators_of(info enum_type) → span<info>All enumerators of an enum typeC++26
nonstatic_data_members_of(info class_type) → span<info>Non-static data membersC++26
base_classes_of(info class_type) → span<info>Direct base class reflectionsC++26
member_functions_of(info class_type) → span<info>All member functions (incl. special)C++26
parameters_of(info function) → span<info>Function parameter reflectionsC++26
return_type_of(info function) → infoReflection of the return typeC++26
type_of(info member_or_param) → infoType reflection of a member/paramC++26
identifier_of(info entity) → string_viewUnqualified name as a stringC++26
qualified_name_of(info entity) → stringFully qualified nameC++26
is_type(info r) → boolTrue if r reflects a typeC++26
is_class_type(info r) → boolTrue if r reflects a class/structC++26
is_enum_type(info r) → boolTrue if r reflects an enumC++26
is_public(info r) → boolTrue if access is publicC++26
is_static(info r) → boolTrue if the member is staticC++26
is_virtual(info r) → boolTrue if the function/base is virtualC++26
has_default_constructor(info class_type) → boolTrue if default constructor existsC++26
is_final(info class_type) → boolTrue if class is marked finalC++26

Most functions that accept a class type will throw (in consteval context, causing a compile error) if passed a non-class reflection — so pair them with is_class_type guards in generic code.

Compiler Support Status

P2996 is accepted for C++26 but implementer support is still in progress (as of 2026):

CompilerStatus
EDG (Electric Fence)Reference implementation — P2996 experimental branch
ClangExperimental (-freflection, not upstreamed yet)
GCCUnder development
MSVCUnder development

Use EDG's P2996 sandbox or the experimental Compiler Explorer branches to try the syntax today.

Trying P2996 on Compiler Explorer (Godbolt)

Compiler Explorer hosts a patched Clang fork that implements most of P2996. You can experiment without installing anything locally.

Steps:

  1. Go to godbolt.org and click Add new compiler.
  2. In the compiler search box, type clang-p2996 — Godbolt maintains a dedicated build of the Bloomberg/Clang reflection fork.
  3. In the Compiler options field, add:
    cpp
    -std=c++26 -freflection -freflection-new-syntax
    The -freflection-new-syntax flag enables the ^^ double-caret operator (P3381R0). Without it the old single-caret syntax is used instead.
  4. Add #include <meta> at the top of your source and start experimenting.
cpp
// Godbolt quick-start — paste this into the editor with clang-p2996
// Flags: -std=c++26 -freflection -freflection-new-syntax
#include <meta>
#include <print>

enum class Dir { North, South, East, West };

int main() {
    template for (constexpr auto e : std::meta::enumerators_of(^^Dir)) {
        std::print("{}\n", std::meta::identifier_of(e));
    }
}
// Output: North  South  East  West

Bloomberg Clang fork. The canonical reflection implementation lives at github.com/bloomberg/clang-p2996. It tracks the paper closely and is the basis for the Godbolt clang-p2996 compiler. Features available there include: ^^ reflections, [: :] splices, nonstatic_data_members_of, enumerators_of, base_classes_of, parameters_of, and expansion statements (template for). The define_aggregate API is partially implemented but may require the latest branch build.

Circle compiler. The Circle C++ compiler by Sean Baxter is an independent implementation with its own reflection-like introspection system. Circle's syntax differs from P2996 in several places, so code written for one does not run unmodified on the other. Circle is useful for exploring the design space but is not a conformance target for standard C++26 reflection.

Best practice. Always pin the exact paper revision and compiler flags in a comment at the top of reflection-heavy files. The API surface of std::meta changed substantially between P2996R3 and R7; code that worked in an older Godbolt snapshot may not compile against a newer build.

Key Rules

  • ^^ only works in a consteval or constexpr context; reflection values cannot persist at runtime
  • [:r:] splicing must produce a syntactically valid construct at the splice site
  • std::meta::info is a scalar type — you can store it in constexpr arrays and pass it to consteval functions
  • template for (expansion statements, P1306) is the idiomatic way to iterate over reflected member ranges
  • Reflection observes the declaration, not the definition — a reflected type doesn't require a complete definition for queries like name_of
  • ^^:: reflects the global namespace; ^^std reflects the std namespace — useful for iterating top-level declarations
  • ^^ and ^ (XOR) coexist without ambiguity: ^^T is always a reflection, a ^ b is always XOR