Back to blog

C++26 Reflection: A Practical Guide to Compile-Time Introspection

January 6, 2026 12 min read
cppcpp26reflectionmetaprogrammingcompile-time

What Is Reflection?

Reflection lets your code examine its own structure at compile time. You can query type information, iterate over class members, and generate code based on what you discover—all without complex template metaprogramming patterns.

The proposal was voted into C++26 in June 2025. While the standard won't be finalized until later this year, experimental implementations are already available for testing.

The Core Concepts

C++26 reflection introduces two operators and a type that let you work with metadata.

The Reflection Operator (^)

The reflection operator captures metadata about a program element:

#include <experimental/meta>

int main() {
    constexpr auto type_info = ^int;  // Reflect the type 'int'
    constexpr auto var_info = ^std::vector;  // Reflect a template
}

The Splice Operator ([: :])

The splice operator converts reflection values back into code:

constexpr auto r = ^int;
typename[:r:] x = 42;  // Same as: int x = 42;

typename[:^char:] c = '*';  // Same as: char c = '*';

The std::meta::info Type

All reflection values have type std::meta::info. This opaque design allows future extensions without breaking existing code. You'll typically pass these values to metafunctions rather than work with them directly.

Practical Examples

Enum to String Conversion

Automatic enum-to-string conversion is straightforward with reflection:

#include <experimental/meta>
#include <string>
#include <optional>

template <typename E>
requires std::is_enum_v<E>
constexpr std::string enum_to_string(E value) {
    template for (constexpr auto e : std::meta::enumerators_of(^E)) {
        if (value == [:e:]) {
            return std::string(std::meta::identifier_of(e));
        }
    }
    return "<unnamed>";
}

enum class Color { Red, Green, Blue };

int main() {
    static_assert(enum_to_string(Color::Red) == "Red");
    static_assert(enum_to_string(Color::Green) == "Green");
}

The std::meta::enumerators_of function returns all enumerators at compile time. We iterate with a template for loop (new in C++26), comparing values and extracting names with std::meta::identifier_of.

Struct Introspection

Iterating over struct members:

#include <experimental/meta>
#include <iostream>
#include <array>

struct Person {
    int age;
    std::string name;
    double height;
};

template <typename T>
consteval auto get_member_names() {
    constexpr auto members = std::meta::nonstatic_data_members_of(^T);
    std::array<std::string_view, members.size()> names;
    
    for (size_t i = 0; i < members.size(); ++i) {
        names[i] = std::meta::identifier_of(members[i]);
    }
    
    return names;
}

int main() {
    constexpr auto names = get_member_names<Person>();
    
    for (const auto& name : names) {
        std::cout << name << '\n';
    }
    // Output: age, name, height
}

Generating SQL Queries

Generating SQL INSERT statements from C++ structs:

#include <experimental/meta>
#include <string>
#include <sstream>

template <typename T>
std::string generate_insert_columns() {
    std::ostringstream oss;
    constexpr auto members = std::meta::nonstatic_data_members_of(^T);
    
    bool first = true;
    template for (constexpr auto member : members) {
        if (std::meta::is_public(member)) {
            if (!first) oss << ", ";
            oss << std::meta::identifier_of(member);
            first = false;
        }
    }
    
    return oss.str();
}

struct User {
    int id;
    std::string name;
    double balance;
private:
    int secret;  // This will be excluded
};

int main() {
    std::string columns = generate_insert_columns<User>();
    std::cout << "INSERT INTO users (" << columns << ") VALUES (?, ?, ?)\n";
    // Output: INSERT INTO users (id, name, balance) VALUES (?, ?, ?)
}

The std::meta::is_public(member) check automatically excludes private members—something that previously required boilerplate or external tools.

Member Layout and Offsets

Reflection provides access to low-level details like member offsets and sizes—useful for serialization, memory mapping, and FFI:

#include <experimental/meta>
#include <array>

struct member_descriptor {
    std::size_t offset;
    std::size_t size;
};

template <typename S>
consteval auto get_layout() {
    constexpr auto members = std::meta::nonstatic_data_members_of(^S);
    std::array<member_descriptor, members.size()> layout;
    
    for (size_t i = 0; i < members.size(); ++i) {
        layout[i] = {
            .offset = std::meta::offset_of(members[i]),
            .size = std::meta::size_of(members[i])
        };
    }
    
    return layout;
}

struct DataPacket {
    char header;
    int payload;
    double timestamp;
};

int main() {
    constexpr auto layout = get_layout<DataPacket>();
    
    for (size_t i = 0; i < layout.size(); ++i) {
        std::cout << "Member " << i 
                  << ": offset=" << layout[i].offset 
                  << ", size=" << layout[i].size << '\n';
    }
}

This layout information is computed at compile time, enabling zero-overhead abstractions for binary protocols and serialization.

Key Metafunctions

The reflection API provides metafunctions for querying and manipulating type information:

Type and Member Queries:

  • std::meta::nonstatic_data_members_of(type) — returns all non-static data members
  • std::meta::enumerators_of(enum_type) — returns all enumerators
  • std::meta::members_of(type) — returns all members including functions
  • std::meta::bases_of(type) — returns base classes

Information Extraction:

  • std::meta::identifier_of(reflection) — gets the name as a string
  • std::meta::type_of(reflection) — gets the type of a member
  • std::meta::offset_of(member) — gets byte offset
  • std::meta::size_of(reflection) — gets size in bytes

Type Testing:

  • std::meta::is_public(reflection) — checks public access
  • std::meta::is_protected(reflection) — checks protected access
  • std::meta::is_private(reflection) — checks private access
  • std::meta::test_type<Trait>(type) — applies type traits

All functions are consteval, executing entirely at compile time with zero runtime overhead.

Reflection vs. Template Metaprogramming

Compare approaches for checking if a class has a specific member function:

Traditional Template Metaprogramming:

template <typename T, typename = void>
struct has_serialize : std::false_type {};

template <typename T>
struct has_serialize<T, std::void_t<
    decltype(std::declval<T>().serialize())
>> : std::true_type {};

With C++26 Reflection:

template <typename T>
consteval bool has_serialize() {
    for (auto member : std::meta::members_of(^T)) {
        if (std::meta::identifier_of(member) == "serialize") {
            return true;
        }
    }
    return false;
}

The reflection version reads like procedural code. No SFINAE, no void_t, no template arcana. Error messages are comprehensible when things go wrong.

Performance

Reflection is a zero-overhead abstraction. All operations happen at compile time through consteval functions. By runtime, the compiler has generated optimized code based on the introspection—identical to hand-written alternatives.

Experimental implementations on Compiler Explorer verify that reflection-based code compiles to the same machine code as manual implementations.

Trying Reflection Today

Experimental implementations are available now:

Bloomberg's Clang Fork: Available on GitHub and Compiler Explorer. The most complete implementation supporting most P2996 features.

EDG Compiler: Also available on Compiler Explorer.

To use these, enable experimental flags and include <experimental/meta>:

// Compile with: clang++ -std=c++2c -freflection-latest
#include <experimental/meta>

Current Limitations

P2996 is the initial reflection facility. Some features aren't included in C++26:

Code Injection: You can introspect existing code, but generating new functions or classes requires additional proposals (like P2237, possibly in C++29).

String-Based Lookup: You can't look up members by string variable. Names come from reflection queries.

Runtime Reflection: Everything happens at compile time. Runtime type information beyond RTTI must be built using compile-time reflection.

These represent careful design choices to deliver an implementable feature set while leaving room for future enhancements.

Applications

Reflection enables:

Serialization: Convert objects to JSON, XML, or binary formats without macros or code generation.

ORM Systems: Map C++ structs to database tables with minimal boilerplate.

Command-Line Parsers: Generate argument parsers from struct definitions.

Debug Logging: Create comprehensive output with member names and values.

Unit Testing: Generate test fixtures and comparison operators.

Network Protocols: Define message structures once and handle serialization automatically.

Looking Ahead

Reflection in C++26 is the foundation. Future standards will build on it with token injection, enhanced code generation, and expanded introspection.

The proposal establishes a solid core that can be extended incrementally in future C++ releases.

Conclusion

C++26 reflection provides compile-time introspection through intuitive operators and a clean API. It makes metaprogramming techniques more accessible while maintaining zero-overhead abstractions.

Whether building serialization libraries, code generation tools, or cleaner type-handling code, reflection changes how you approach C++ metaprogramming. Experimental implementations are available now—start experimenting to be ready when C++26 arrives.