C++ programmers have spent decades writing the same boilerplate by hand: enum-to-string tables, serializers, ORM mappings, and hash functions that list every member. C++26 static reflection ends that. The compiler now exposes the structure of your types as constant-evaluated values, and you can turn those values back into code, all before the program runs.
Quick Takeaways
- The
^^operator turns a type, member, or namespace into a value of typestd::meta::info. - The splice
[: r :]turns a reflection value back into code. - Queries live in the
<meta>header understd::metaand run inconstevalcontext, so they cost nothing at runtime. template for(expansion statements) iterates over reflected members without recursive templates.- Compiler support is still maturing. Check the status of GCC 16 (
-freflection) and the Bloomberg Clang fork before you commit to production use.
| Capability | Syntax | Runtime Cost |
|---|---|---|
| Reflect an entity | ^^T |
Zero |
| Splice back to code | [: r :] |
Zero |
| Query members | std::meta::nonstatic_data_members_of(...) |
Zero (consteval) |
| Iterate over members | template for (...) |
Zero (unrolled) |
| Generate a type | std::meta::define_aggregate(...) |
Zero |
What Is Static Reflection in C++26?
Static reflection is the ability of a program to inspect its own declarations during compilation. The C++26 design (proposal P2996) adds a reflection operator, splicing, and a metafunction library. It does not add runtime type information. Everything resolves at compile time, which preserves the zero-overhead principle.
Two ideas carry the whole feature:
- Reflection values.
^^Xproduces an opaque, comparable constant of typestd::meta::info. - Metafunctions. Plain
constevalfunctions take and returninfovalues and answer questions like “what are the members of this struct?”
Setting Up Your Compiler
Reflection is the newest major C++26 feature, so toolchain support is uneven. Verify these flags against your compiler’s current documentation:
# GCC 16+ (experimental flag)
g++ -std=c++26 -freflection main.cpp -o main
# Bloomberg's Clang fork (check its README for exact flags)
clang++ -std=c++26 -freflection main.cpp -o main
Test for the feature with the standard macro __cpp_impl_reflection (the exact macro name can vary while implementations stabilize). Gate your reflection code behind it so builds on older compilers still pass.
Syntax Breakdown
The Reflection Operator ^^
The ^^ operator takes a type, namespace, enumerator, or member and yields a std::meta::info value.
#include <meta>
struct Point { int x; int y; };
constexpr std::meta::info r_type = ^^Point; // reflects the type
constexpr std::meta::info r_member = ^^Point::x; // reflects a data member
constexpr std::meta::info r_int = ^^int; // reflects a built-in type
Each constant is an ordinary value. You can store it, compare it with ==, and pass it to consteval functions. You cannot use it at runtime, because info exists only inside constant evaluation.
The Splice [: r :]
A splice converts a reflection back into the entity it names.
constexpr auto r = ^^int;
[:r:] value = 42; // same as: int value = 42;
For a member reflection, splice it into a member-access expression:
Point p{3, 4};
constexpr auto rx = ^^Point::x;
int a = p.[:rx:]; // same as: p.x
The compiler evaluates rx at compile time, so p.[:rx:] compiles to a direct member access with no lookup.
Querying Entities with std::meta
Metafunctions answer structural questions. These are the ones you will use most:
| Metafunction | Returns | Typical Use |
|---|---|---|
identifier_of(r) |
std::string_view |
Name of a member or enumerator |
nonstatic_data_members_of(r, ctx) |
std::vector<info> |
Fields of a struct |
enumerators_of(r) |
std::vector<info> |
Values of an enum |
type_of(r) |
info |
Type of a member |
is_public(r) |
bool |
Access checks |
define_aggregate(r, specs) |
info |
Generate a struct body |
The access_context argument controls which members you may see. Use std::meta::access_context::current() to respect the access rules at your call site, or unchecked() for tooling that must see private members.
Practical Implementation
Example 1: Enum to String Without Macros
This is the classic reflection use case. You no longer need an X-macro or a hand-written switch.
#include <meta>
#include <string_view>
#include <type_traits>
template <typename E>
requires std::is_enum_v<E>
constexpr std::string_view enum_to_string(E value) {
// define_static_array promotes the vector to static storage,
// which template-for requires for constant iteration.
template for (constexpr auto e :
std::define_static_array(std::meta::enumerators_of(^^E))) {
if (value == [:e:]) {
return std::meta::identifier_of(e);
}
}
return "<unnamed>";
}
enum class Color { Red, Green, Blue };
static_assert(enum_to_string(Color::Green) == "Green");
What happens: The compiler unrolls the template for loop into three comparisons, one per enumerator. [:e:] splices each reflection into its real enumerator value. The function runs in constexpr context, and static_assert proves it at compile time. Adding Yellow to the enum updates the function automatically.
Example 2: Serialize Any Struct to JSON
Serialization code is where reflection saves the most boilerplate. This version works on any aggregate whose members are numbers, booleans, or strings.
#include <meta>
#include <string>
#include <type_traits>
// Leaf serializers
inline std::string to_json_value(int v) { return std::to_string(v); }
inline std::string to_json_value(double v) { return std::to_string(v); }
inline std::string to_json_value(bool v) { return v ? "true" : "false"; }
inline std::string to_json_value(const std::string& v){ return "\"" + v + "\""; }
template <typename T>
std::string to_json(const T& obj) {
std::string out = "{";
bool first = true;
template for (constexpr auto member : std::define_static_array(
std::meta::nonstatic_data_members_of(
^^T, std::meta::access_context::current()))) {
if (!first) out += ", ";
first = false;
out += '"';
out += std::meta::identifier_of(member); // field name at compile time
out += "\": ";
out += to_json_value(obj.[:member:]); // field value via splice
}
return out + "}";
}
struct User {
int id;
std::string name;
bool active;
};
Calling to_json(User{7, "Ada", true}) returns:
{"id": 7, "name": "Ada", "active": true}
What happens: Each iteration is stamped out at compile time with its own member constant. The field names become string literals in the binary, and each obj.[:member:] is a direct member read. The resulting machine code matches a hand-written serializer.
Example 3: Generate a Struct at Compile Time
Reflection can also create declarations. define_aggregate completes a forward-declared struct from a list of member specs.
#include <meta>
struct Vec2; // incomplete type
consteval {
std::meta::define_aggregate(^^Vec2, {
std::meta::data_member_spec(^^float, {.name = "x"}),
std::meta::data_member_spec(^^float, {.name = "y"}),
});
}
// Vec2 is now equivalent to: struct Vec2 { float x; float y; };
constexpr Vec2 origin{0.0f, 0.0f};
What happens: The consteval { ... } block runs during translation and completes Vec2 before its first use. In real projects, you would feed this a computed member list. For example, you could read a schema and build a matching struct, or generate a “patch” type where every field becomes std::optional.
Example 4: Drive Behavior with Annotations
C++26 also adds attribute-style annotations (P3394) that attach values to declarations. You can read them back through reflection to configure generated code.
#include <meta>
struct Rename { const char* name; };
struct Account {
[[=Rename{"account_id"}]] int id;
int balance;
};
Inside a template for, query a member’s annotations with std::meta::annotations_of and read the payload with std::meta::extract<Rename>(...). A JSON serializer can then emit account_id instead of id. Treat the exact helper signatures as subject to change while the library settles, and check your compiler’s <meta> header.
Edge Cases and Performance Trade-offs
Compile-Time Cost
Reflection moves work from runtime to the compiler. Large template for loops and heavy std::vector<info> queries increase build time. Keep these habits:
- Cache expensive queries in a
constexprvariable. - Put reflection-heavy helpers in a single translation unit or module.
- Avoid reflecting entire large namespaces when you need one type.
Access and Encapsulation
Reflection respects access control by default. Passing access_context::unchecked() lets you read private members. That is useful for serialization frameworks and dangerous elsewhere, so confine it to code you control and review.
Constant-Evaluation Limits
std::meta::info values cannot escape to runtime. Code like std::meta::info r = ...; inside a non-consteval function with no constant initializer will not compile. Always keep reflection values in constexpr or consteval contexts, and convert them to std::string_view or bool before leaving.
Reflection vs. Pre-C++26 Techniques
| Technique | Boilerplate | Compile-Time Cost | Type Safety | Maintenance |
|---|---|---|---|---|
| X-macros | Medium | Low | Weak (text substitution) | Error-prone |
| Template metaprogramming | High | High | Strong | Hard to read |
| External codegen (Python, protoc) | Low in source | Extra build step | Strong | Needs separate toolchain |
| Third-party libs (Boost.PFR, magic_enum) | Low | Medium | Strong | Relies on compiler quirks |
| C++26 static reflection | Lowest | Medium | Strong, standardized | Easy |
Libraries like magic_enum and Boost.PFR depend on implementation tricks such as parsing __PRETTY_FUNCTION__. Reflection replaces those tricks with a portable standard interface.
Best Practices: Bad Code vs. Good Code
Anti-Pattern: Hand-Maintained Enum Table
// BAD: every new enumerator needs an edit in two places
enum class Status { Ok, Failed, Pending };
constexpr const char* to_string(Status s) {
switch (s) {
case Status::Ok: return "Ok";
case Status::Failed: return "Failed";
// Forgot Pending: silent bug, falls through to undefined behavior
}
}
Refactored: Reflection-Driven
// GOOD: one definition, zero drift
enum class Status { Ok, Failed, Pending };
static_assert(enum_to_string(Status::Pending) == "Pending");
// New enumerators appear in the output automatically.
Anti-Pattern: Copy-Paste Serializers
// BAD: add a field to User and this silently goes stale
std::string to_json(const User& u) {
return "{\"id\": " + std::to_string(u.id) +
", \"name\": \"" + u.name + "\"}"; // 'active' is missing
}
Refactored: Generic to_json<T>
// GOOD: the generic to_json<T> from Example 2 covers every member
auto json = to_json(User{7, "Ada", true});
Rules of thumb:
- Reflect once, store the result in a
constexprvariable, and reuse it. - Prefer
template forover recursive templates. It reads better and compiles faster. - Write unit tests with
static_assertso a reflection regression fails the build. - Keep reflection code behind small, named helpers (
to_json,enum_to_string) instead of scattering^^and[: :]through business logic.
Where Reflection Fits in Real Projects
Reflection pays off most in code that mirrors a type’s structure:
- Serialization and deserialization (JSON, binary, protobuf-style formats)
- ORM and database mapping from struct fields to columns
- Command-line parsers that build flags from a config struct
- Test frameworks that discover and register test types
- Dependency injection that inspects constructor parameters
- Language bindings that expose C++ classes to Python or JavaScript
Start small. Replace one macro-based enum table, measure build time, and expand from there.
FAQ
What is static reflection in C++26?
Static reflection is a compile-time mechanism that lets C++ code inspect declarations such as members, enumerators, and types, then generate code from that information. It uses the ^^ operator to get a std::meta::info value and [: :] to splice it back into code.
Does C++26 reflection have runtime overhead?
No. Reflection queries run in consteval context, and the results become ordinary literals and member accesses in the compiled program. Costs appear in compile time, not runtime.
Which compilers support C++26 reflection?
Experimental support has landed in GCC 16 (behind -freflection) and in the Bloomberg Clang fork. Mainstream Clang and MSVC support varies by version, so check each vendor’s release notes and test with the feature-test macro.
How is C++26 reflection different from RTTI?
RTTI (typeid, dynamic_cast) is a runtime facility with limited information, mainly type identity and inheritance. Static reflection is compile-time and exposes members, names, enumerators, and annotations. It can also generate new types, which RTTI cannot.




