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 ^ (splice-specifier) 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
    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

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.

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