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 ^^ (reflection) 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 // 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.
#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 |
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.
// 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 comparableThe 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.
#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: trueFor 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.
#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.
#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.
#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 ^^.
| Function | Signature (simplified) | Returns | Standard |
|---|---|---|---|
enumerators_of | (info enum_type) → span<info> | All enumerators of an enum type | C++26 |
nonstatic_data_members_of | (info class_type) → span<info> | Non-static data members | C++26 |
base_classes_of | (info class_type) → span<info> | Direct base class reflections | C++26 |
member_functions_of | (info class_type) → span<info> | All member functions (incl. special) | C++26 |
parameters_of | (info function) → span<info> | Function parameter reflections | C++26 |
return_type_of | (info function) → info | Reflection of the return type | C++26 |
type_of | (info member_or_param) → info | Type reflection of a member/param | C++26 |
identifier_of | (info entity) → string_view | Unqualified name as a string | C++26 |
qualified_name_of | (info entity) → string | Fully qualified name | C++26 |
is_type | (info r) → bool | True if r reflects a type | C++26 |
is_class_type | (info r) → bool | True if r reflects a class/struct | C++26 |
is_enum_type | (info r) → bool | True if r reflects an enum | C++26 |
is_public | (info r) → bool | True if access is public | C++26 |
is_static | (info r) → bool | True if the member is static | C++26 |
is_virtual | (info r) → bool | True if the function/base is virtual | C++26 |
has_default_constructor | (info class_type) → bool | True if default constructor exists | C++26 |
is_final | (info class_type) → bool | True if class is marked final | C++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):
| 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.
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:
- Go to godbolt.org and click Add new compiler.
- In the compiler search box, type clang-p2996 — Godbolt maintains a dedicated build of the Bloomberg/Clang reflection fork.
- In the Compiler options field, add:
Thecpp
-std=c++26 -freflection -freflection-new-syntax-freflection-new-syntaxflag enables the^^double-caret operator (P3381R0). Without it the old single-caret syntax is used instead. - Add
#include <meta>at the top of your source and start experimenting.
// 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 WestBloomberg 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 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 ^^::reflects the global namespace;^^stdreflects thestdnamespace — useful for iterating top-level declarations^^and^(XOR) coexist without ambiguity:^^Tis always a reflection,a ^ bis always XOR