Static Reflection in C++26: Generate Code at Compile Time

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 type std::meta::info.
  • The splice [: r :] turns a reflection value back into code.
  • Queries live in the <meta> header under std::meta and run in consteval context, 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:

  1. Reflection values. ^^X produces an opaque, comparable constant of type std::meta::info.
  2. Metafunctions. Plain consteval functions take and return info values 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 constexpr variable.
  • 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 constexpr variable, and reuse it.
  • Prefer template for over recursive templates. It reads better and compiles faster.
  • Write unit tests with static_assert so 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.

Hot this week

The State of Robotics in 2026: 10 Biggest Developments

The 10 biggest robotics developments of 2026: whole-body VLA models, humanoid safety, ROS 2 Lyrical Luth, and Jetson Thor. Get the data and code.

EU Machinery Regulation 2027: What Robot Builders Need to Know

Building robots for the EU? Regulation (EU) 2023/1230 applies from 20 Jan 2027. Get the cybersecurity, AI, and CE marking checklist now.

ISO 10218:2025 Explained: The New Industrial Robot Safety Standard

ISO 10218:2025 rewrites industrial robot safety: Class I/II robots, built-in cobot limits, cybersecurity. Get the checklist and ROS2 code. Read now.

NVIDIA Jetson Orin Nano, AGX Orin, and Thor: Which One for Your Robot?

Jetson Orin Nano vs AGX Orin vs Thor: compare TOPS, memory bandwidth, power, and price to pick the right robot compute. Read the guide.

ROS 2 Distributions Explained: Humble, Jazzy, Kilted, and Lyrical (Which to Use)

Compare ROS 2 Humble, Jazzy, Kilted, and Lyrical by EOL date, platform support, and features. Pick the right distro for your robot. Read the guide.

Topics

The State of Robotics in 2026: 10 Biggest Developments

The 10 biggest robotics developments of 2026: whole-body VLA models, humanoid safety, ROS 2 Lyrical Luth, and Jetson Thor. Get the data and code.

EU Machinery Regulation 2027: What Robot Builders Need to Know

Building robots for the EU? Regulation (EU) 2023/1230 applies from 20 Jan 2027. Get the cybersecurity, AI, and CE marking checklist now.

ISO 10218:2025 Explained: The New Industrial Robot Safety Standard

ISO 10218:2025 rewrites industrial robot safety: Class I/II robots, built-in cobot limits, cybersecurity. Get the checklist and ROS2 code. Read now.

NVIDIA Jetson Orin Nano, AGX Orin, and Thor: Which One for Your Robot?

Jetson Orin Nano vs AGX Orin vs Thor: compare TOPS, memory bandwidth, power, and price to pick the right robot compute. Read the guide.

ROS 2 Distributions Explained: Humble, Jazzy, Kilted, and Lyrical (Which to Use)

Compare ROS 2 Humble, Jazzy, Kilted, and Lyrical by EOL date, platform support, and features. Pick the right distro for your robot. Read the guide.

Build a Low-Cost AI Robot Arm With SO-101 and LeRobot

Build an SO-101 robot arm under $250, calibrate it, record demos, and train an ACT policy with LeRobot. Follow the full guide and start building.

Robot Foundation Models: GR00T, pi, Gemini Robotics, and Open Alternatives Compared

Compare robot foundation models: NVIDIA GR00T, Physical Intelligence π, Gemini Robotics 2, and open VLAs. Get latency math, code, and a pick guide.

How Much Does a Humanoid Robot Cost? Prices, Subscriptions, and Hidden Costs

Humanoid robot cost in 2026: prices from $4,900, $499/mo subscriptions, and hidden fees. See the full TCO breakdown and compare models now.

Related Articles

Popular Categories