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++26Static 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:
| Concept | Meaning |
|---|---|
^T | Reflect on type T — produces a std::meta::info |
^expr | Reflect 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
#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.
#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
#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)
#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:
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
#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
| Feature | TMP (pre-C++26) | Reflection (C++26) |
|---|---|---|
| Access member names | Impossible without macros | std::meta::name_of(m) |
| Iterate struct fields | X-Macro or manual | nonstatic_data_members_of |
| Count enum values | Magic-enum hacks | std::meta::enumerators_of |
| Access field types | Requires explicit traits | std::meta::type_of(m) |
| Readability | Template soup | consteval functions |
Compiler Support Status
P2996 is accepted for C++26 but implementer support is still in progress (as of 2026):
| Compiler | Status |
|---|---|
| EDG (Electric Fence) | Reference implementation — P2996 experimental branch |
| Clang | Experimental (-freflection, not upstreamed yet) |
| GCC | Under development |
| MSVC | Under development |
Use EDG's P2996 sandbox or the experimental Compiler Explorer branches to try the syntax today.
Key Rules
^only works in aconstevalorconstexprcontext; reflection values cannot persist at runtime[:r:]splicing must produce a syntactically valid construct at the splice sitestd::meta::infois a scalar type — you can store it inconstexprarrays and pass it toconstevalfunctionstemplate 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