C++Generic Programming

C++26 Static Reflection: Compile-Time Introspection, Auto-Serialization & Zero-Overhead ORM

TT
TopicTrick Team
C++26 Static Reflection: Compile-Time Introspection, Auto-Serialization & Zero-Overhead ORM

C++26 Static Reflection: Compile-Time Introspection, Auto-Serialization & Zero-Overhead ORM


Table of Contents


Static vs Runtime Reflection


The Reflection Operator: ^

In the C++26 reflection proposal (P2996), ^ applied to a type/expression produces a compile-time reflection object (a value of type std::meta::info):

cpp
// C++26 — requires compiler with P2996 support (EDG/Circle/Clang experimental)
#include <meta>

struct User {
    int id;
    std::string name;
    float score;
};

// ^ operator produces compile-time metadata:
constexpr auto user_meta   = ^User;       // Reflection of the User type
constexpr auto member_list = members_of(^User); // Compile-time list of members

// Inspect type name:
constexpr std::string_view type_name = name_of(^User); // "User"

// typeof: get the type from a reflection object
using T = typename[:type_of(^User::id):]; // int

std::meta: Navigating the Metadata

The <meta> header provides functions to navigate reflection objects:

cpp
// P2996 meta functions (proposed for C++26):
namespace std::meta {
    // Query functions:
    consteval std::string_view name_of(info r);          // Name of the entity
    consteval std::string_view qualified_name_of(info r); // Full qualified name
    consteval info type_of(info r);                       // Type of a member
    consteval std::size_t size_of(info r);                // sizeof for a type
    consteval std::size_t offset_of(info r);              // offsetof for a member
    
    // Navigation:
    consteval std::span<const info> members_of(info r);    // All data members
    consteval std::span<const info> bases_of(info r);      // Base classes
    consteval std::span<const info> enumerators_of(info r); // Enum values
    
    // Classification:
    consteval bool is_class(info r);
    consteval bool is_enum(info r);
    consteval bool is_member(info r);
    consteval bool is_static_member(info r);
    consteval bool is_public(info r);
}

// Usage:
constexpr auto members = std::meta::members_of(^User);
// members[0] = reflection of User::id (int)
// members[1] = reflection of User::name (std::string)
// members[2] = reflection of User::score (float)

template for: Iterating Members at Compile Time

template for is a new C++26 loop that iterates a compile-time range, generating separate code for each iteration — like loop unrolling at the source level:

cpp
// Iterate all members of User:
void describe_type() {
    template for (constexpr auto member : std::meta::members_of(^User)) {
        // Each iteration: member is a constexpr reflection of one field
        std::println("Field: {} ({})",
            std::meta::name_of(member),
            std::meta::name_of(std::meta::type_of(member)));
    }
}
// Generates:
// std::println("Field: {} ({})", "id", "int");
// std::println("Field: {} ({})", "name", "std::string");
// std::println("Field: {} ({})", "score", "float");
// All at compile time — loop is unrolled!

Automatic JSON Serialization

The classic use case: serialize any struct to JSON without writing a single mapping:

cpp
#include <meta>
#include <string>
#include <format>

// Universal to_json — works for ANY struct:
template<typename T>
std::string to_json(const T& obj) {
    std::string result = "{";
    bool first = true;
    
    template for (constexpr auto member : std::meta::members_of(^T)) {
        if (!first) result += ", ";
        first = false;
        
        // Get the field name as a compile-time string:
        constexpr std::string_view field_name = std::meta::name_of(member);
        
        // Get the field value using splice [:member:] syntax:
        const auto& field_value = obj.[:member:];
        
        result += std::format("\"{}\":{}", field_name,
                              to_json_value(field_value)); // recursive for nested
    }
    
    return result + "}";
}

// Helper for value serialization:
template<typename T>
std::string to_json_value(const T& val) {
    if constexpr (std::is_arithmetic_v<T>) {
        return std::to_string(val);
    } else if constexpr (std::is_same_v<T, std::string>) {
        return std::format("\"{}\"", val);
    } else {
        return to_json(val); // Recurse for nested structs
    }
}

// Usage — zero boilerplate:
User u{1, "Alice", 98.5f};
std::println("{}", to_json(u));
// Output: {"id":1, "name":"Alice", "score":98.500000}

// Compare to what you'd write manually:
// if (name == "id") ...
// else if (name == "name") ...
// else if (name == "score") ...
// → ELIMINATED by reflection

Production Today: Boost.PFR and reflect-cpp

C++26 isn't finalized yet. For production code today, use these libraries:

Boost.PFR: Struct Reflection Without Macros (C++17)

PFR (Precise Flat Reflection) uses structured bindings to iterate struct members at compile time — without C++26, without macros:

cpp
#include <boost/pfr.hpp>

struct User {
    int id;
    std::string name;
    float score;
};

User u{1, "Alice", 98.5f};

// Get number of fields:
constexpr size_t count = boost::pfr::tuple_size_v<User>; // 3

// Access by index:
auto& id   = boost::pfr::get<0>(u); // 1
auto& name = boost::pfr::get<1>(u); // "Alice"

// Iterate all fields (C++17):
boost::pfr::for_each_field(u, [](const auto& field, std::size_t idx) {
    std::println("Field {}: {}", idx, field);
});
// Output:
// Field 0: 1
// Field 1: Alice
// Field 2: 98.5

// Serialize to JSON using PFR:
template<typename T>
std::string pfr_to_string(const T& obj) {
    std::string result;
    boost::pfr::for_each_field(obj, [&](const auto& field, std::size_t i) {
        if (i > 0) result += ", ";
        result += std::format("{}", field);
    });
    return result;
}
// Limitation: PFR can't access FIELD NAMES (only values)
// Need C++26 reflection or reflect-cpp for names

reflect-cpp: Full-Featured Reflection Library

cpp
#include <rfl.hpp>        // reflect-cpp
#include <rfl/json.hpp>

struct User {
    int id;
    std::string name;
    float score;
};

// Automatic JSON serialization with field names:
User u{1, "Alice", 98.5f};
std::string json = rfl::json::write(u);
// {"id":1,"name":"Alice","score":98.5}

// Automatic JSON deserialization:
std::string input = R"({"id":2,"name":"Bob","score":75.0})";
auto user = rfl::json::read<User>(input);
if (user) {
    std::println("{}: {}", user->name, user->score);
}

// Also supports: XML, YAML, TOML, CBOR, MessagePack, BSON

Comparison: C++26 vs Java/C# Reflection

AspectJava Runtime ReflectionC# Runtime ReflectionC++26 Static Reflection
WhenRuntimeRuntimeCompile-time
Overhead per call~100ns~50ns0ns
Type safetyCast at runtimeCast at runtimeCompile-time
Field namesFrom JVM metadataFrom CLR metadataFrom compiler AST
Private accessWith setAccessible()With BindingFlagsNo (respects access specifiers)
Code gen neededNoNoNo (unlike Protobuf/moc)

Frequently Asked Questions

Is C++26 reflection finalised and available today? P2996 (static reflection) was voted into C++26 in 2024. Full support is available in the EDG frontend (used by IAR, Green Hills) and experimental Clang forks. GCC and mainstream Clang are implementing it. By 2026, all major compilers will support it. For production today, use Boost.PFR (C++17, no macros) or reflect-cpp (C++17, full features including names).

Can reflection access private members? No — C++26 static reflection respects access specifiers. members_of(^T) returns only public members by default. There are proposals for explicit opt-in access to private members (std::meta::accessible_members_of), but the default is access-controlled.

Will reflection replace code generation tools? Yes, largely. Tools like Qt's moc, Protobuf's protoc, and many ORMs exist primarily because C++ lacks introspection. With static reflection, all the logic these tools generate (serialization, property access, signal/slot dispatch) can be implemented as ordinary C++ templates — no code generation step required.


Key Takeaway

C++26 Static Reflection is the final piece that transforms C++ from a "low-level systems language" into a complete ecosystem. It eliminates entire categories of boilerplate: no more manual JSON serialization, no more macro-based ORM mapping, no more code generation for protocol buffers. And unlike Java/C# reflection, it costs exactly zero at runtime — the compiler generates all the inspection code into direct field accesses at compile time. For production systems in 2026, reflect-cpp and Boost.PFR provide the same capability today.

Read next: C++ Modules: Faster Builds & Cleaner Code →


Part of the C++ Mastery Course — 30 modules from modern C++ basics to expert systems engineering.