C++ 26 Reflections

C++
Author

Quasar

Published

March 13, 2026

Introduction

I recently attended a talk on reflections in C++26 by Sarthak Sehgal and much of my code snippets here are inspired by his talk.

What is a reflection really?

A reflection is the ability of software to expose its internal structure. C++ reflections are static reflections - that is, the compiler exposes the structure at compile-time.

#include <iostream>
#include <meta>

enum class Color{ Red, Blue, Green };

int main(){
    std::cout << std::meta::identifier_of(^^Color) << "\n";
    std::cout << std::meta::identifier_of(
        std::meta::enumerators_of(^^Color)[0]
    ) << "\n";
}

Suppose you want to iterate over the elements of the Color enum class or we may want to convert an enum variant to its string representation - could be done using reflections. Or let’s say, you have a struct in C++ and you want to iterate over what the data-members of the struct are. We are essentially inspecting the C++ code that has been written. Essentially, our program is reflecting itself at compile-time.

Reflections before C++26

Before C++26, there were various workarounds used to generate code to achieve reflection-like behavior. Boost Describe is a C++14 library that uses macro magic, that provides reflections in C++14.

Macro magic

Let’ say I define an enum for the three primary colors Red, Green and Blue.

// I define a macro COLORS. This macro takes in a function FUNC
// and invokes FUNC() on three literals Red, Green and Blue.
// So, what this macro expects is the user to provide some sort 
// of a function and it is going to invoke that on these 
// 3 literals.
#define COLORS      \
    FUNC(Red)       \
    FUNC(Blue)      \
    FUNC(Green)

// Next, we have this enum class Color. What I am doing in the body
// of this enum is, I am defining a function FUNC(arg), which always
// appends `,`(comma) to the argument. So, FUNC(Red) would return
// Red,. I pass COLORS as a parameter to FUNC. So, all its going
// to do is, it will expand FUNC(Red) FUNC(Blue) FUNC(Green) as
// Red, Blue, Green,.
enum class Color{
    #define FUNC(name) name,
        COLORS
    #undef FUNC
};

Compiler Explorer

We can actually see the preprocessor output in Compiler Explorer.

We can now write our own enum_to_string function as follows:

std::string enum_to_string(Color color){
    switch(color){
        #define FUNC(name) case Color::name:    \
            return std::string(#name);
            COLORS
        #undef FUNC
        default:
            return "unknown"; 
    }
}

Reflection in C++26

In C++26, we will use the unibrow operator ^^ as the reflection operator. Reflection using the reflection operator maps its operand from the programming domain to the reflections domain. We can think of the code that we write, types we define as living in the programming domain. Reflections of these types live in the reflections domain. These are two different domains. We can reflect many different entities, for example, a variable, a type, an enum, a namespace, struct or a class.

Reflection operator ^^ and the splicer operator.

Consider the following code snippet:

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

The caret-caret ^^ is the new reflection operator introduced in C++26. What this operator does is, it lifts the expression into the reflection domain. The entity being reflected can be a type, variable, function, namespace, enum. Its informally called the unibrow operator. (Because if you type two carets with dot in-between, it looks like a face ^.^).

We are using the reflection expression on the right ^^int in a constexpr context, so it will be evaluated at compile-time.

The type returned by the reflection operator is std::meta::info. This is the universal type returned from reflecting any type whatsoever.

We can go back from the reflection domain to the programming domain by using the splicer operator [:r:].

Meta functions

We can write a simple meta-function to print all of the variants of an enum.

template 
//                                                                      reflect the enum
//                                                                             |
for(constexpr auto c : std::define_static_array(std::meta::enumerators_of(^^Color)))
{
    std::cout << std::meta::identifier_of(c) << "\n";
}

The reflection operator ^^applied to the Color enum reflects the enum. This reflection lives in the reflection domain. In the reflection domain, I can use some nice meta-functions such as std::meta::enumerators_of() which returns a list of reflections for each of the elements in the enum. Note that it does not return the elements themselves, but the reflection of the elements : {^^Color::Red, ^^Color::Blue, ^^Color::Green }.

The template for iterates for each element reflection at compile-time. std::define_static_array takes an array at compile time and promotes it to static storage duration for use at runtime.

To the reflection of each color, I can apply another metafunction identifier_of which takes in a reflection and returns a string_view which is the identifier of that reflection.

What really is a reflection under the hood?

Consider the following code snippet:

class R;

constexpr std::meta::info res1 = ^^R;
constexpr auto print1 = std::meta::is_complete_type(res1);  // false

class R{
    int a;
};

constexpr std::meta::info res2 = ^^R;

constexpr auto print2 = std::meta::is_complete_type(res2);
constexpr auto print3 = std::meta::is_complete_type(res1);

int main(){
    std::cout << "print1 : " << print1 << "\n";
    std::cout << "print2 : " << print2 << "\n";
    std::cout << "print3 : " << print3 << "\n";
}

Compiler Explorer

res1 is the reflection of an incomplete type - R has only been declared. Hence, we all know that print1 is false, because the type is not complete at this point. Once we define the class R, the type is complete. We now reflect the type R once again, res2 is the reflection of the completed type. So, print2 is true. print3 is interesting. We use the same reflection res1 defined before the type was completed. If we query it, for whether the type is complete, after R has been defined, now this returns true, which is surprising.

The C++ compiler constructs an AST(Abstract Syntax Tree) of the entire source code. A reflection is a reference to a node in the AST. This is why when the type is completed, std::is_complete_type(res1) returns true.

Splicers

The splicing operator produces a C++ expression evaluating to the entity represented by the reflection r. typename[:r:] produces a type specifier. template[:r:] produces a template name.

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

The splicer operator is used to go back from the reflection domain to the programming domain.

Writing string_to_enum and enum_to_string functions

#include <iostream>
#include <meta>

// I define a macro COLORS. This macro takes in a function FUNC
// and invokes FUNC() on three literals Red, Green and Blue.
// So, what this macro expects is the user to provide some sort
// of a function and it is going to invoke that on these
// 3 literals.
#define COLORS \
    FUNC(Red)  \
    FUNC(Blue) \
    FUNC(Green)

// Next, we have this enum class Color. What I am doing in the body
// of this enum is, I am defining a function FUNC(arg), which always
// appends `,`(comma) to the argument. So, FUNC(Red) would return
// Red,. I pass COLORS as a parameter to FUNC. So, all its going
// to do is, it will expand FUNC(Red) FUNC(Blue) FUNC(Green) as
// Red, Blue, Green,.
enum class Color {
#define FUNC(name) name,
    COLORS
#undef FUNC
};

template <typename E>
constexpr std::string_view enum_to_string(E e) {
    template for (constexpr auto I : std::define_static_array(std::meta::enumerators_of(^^E))) {
        if (e == [:I:]) 
            return std::meta::identifier_of(I);
    }
    return "unknown";
}

template <typename E>
constexpr std::optional<E> string_to_enum(std::string_view s) {
    static constexpr auto enum_vals = std::define_static_array(std::meta::enumerators_of(^^E));
    template for (constexpr auto I : enum_vals) {
        if (s == std::meta::identifier_of(I)) 
            return [:I:];
    }
    return std::nullopt;
}
int main() {}

Compiler Explorer

Writing an elementary class serializer

We can iterate over the non-static data-members of the class. Let’s write a simple class serializer using this logic.

#include <iostream>
#include <meta>
#include <optional>

struct Point2D {
    double x;
    double y;
};

template <typename T>
void format(T const& t) {
    std::cout << identifier_of(^^T) << "{ ";
    constexpr auto access_ctx = std::meta::access_context::unchecked();

    template for (constexpr auto mem :
                  std::define_static_array(nonstatic_data_members_of(^^T, access_ctx))) {
        std::cout << std::format("{} = {}, ", identifier_of(mem), t.[:mem:]);
    }

    std::cout << "}";
}

int main() { 
    format(Point2D{.x = 3.0, .y = 4.0}); 
}

Compiler Explorer

Generating code

The metafunction that’s used to generate code in C++26 is define_aggregate. define_aggregate takes the reflection of an incomplete class/struct/union type and a list of reflections of data member descriptions and completes the given class type with datab members as described in the given order.

template <reflection_range R = initializer_list<std::meta::info>>
consteval auto define_aggregate(info type_class, R&&) -> std::meta::info;

In order to provide the reflection of a data-member description, there is another metafunction data_member_spec. data_member_spec returns a reflection a data member description for the data-member of a given type.

consteval auto data_member_spec(info type, data_member_options options) -> info;

info type is the type we want, for example, int or char. options can be used to specify the alignment, bit-field width amongst other things.

#include <iostream>
#include <meta>
#include <string>

struct Person;

consteval{
    define_aggregate(^^Person, {
        data_member_spec(^^std::string, { .name = "name" }),
        data_member_spec(^^int, { .name = "age" }),
        data_member_spec(^^double, { .name = "weight" })
    });
}

int main(){}

Compiler Explorer

In the above example, I take an incomplete type Person and then generate code to add various data-members to it.

Implementing a tuple

tuple implementation before C++26

#include <iostream>
#include <string>

namespace dev {
template <typename... Ts>
struct tuple;

template <typename T>
struct tuple<T> {
    T head;

    constexpr tuple(T value) : head(value) {}
};

template <typename Head, typename... Tail>
struct tuple<Head, Tail...> {
    Head head;
    tuple<Tail...> tail;

    constexpr tuple(Head first, Tail... rest) : head{first}, tail{rest...} {}
};

template <size_t N, typename... Types>
struct nth_type;

template <typename First, typename... Rest>
struct nth_type<0, First, Rest...> {
    using type = First;
};

template <size_t N, typename Head, typename... Tail>
struct nth_type<N, Head, Tail...> {
    using type = nth_type<N - 1, Tail...>::type;
};

template <size_t N, typename... Ts>
using nth_type_t = nth_type<N, Ts...>::type;

template <size_t N, typename Head, typename... Tail>
nth_type_t<N, Head, Tail...> get(tuple<Head, Tail...> tup) {
    if constexpr (N == 0) {
        return tup.head;
    } else {
        return get<N - 1, Tail...>(tup.tail);
    }
}

template <typename... Ts>
tuple(Ts...) -> tuple<Ts...>;

}  // namespace dev

int main() {
    dev::tuple tup{std::string("Hello"), 123, 3.14159};
    std::cout << dev::get<0>(tup) << "\n";
    std::cout << dev::get<1>(tup) << "\n";
    std::cout << dev::get<2>(tup) << "\n";
}

Compiler Explorer

tuple implementation using reflections

#include <iostream>
#include <meta>

namespace dev{
    template<typename... Ts>
    struct tuple{
        struct storage;
        consteval{
            define_aggregate(^^storage, {
                data_member_spec(^^Ts)...
            });
        }

        storage data;
        
        tuple()
        : data{}
        {} 

        tuple(Ts const&... values)
        : data{ values... }
        {}
    };

    template <size_t N, typename... Types>
    struct nth_type{
        static constexpr std::array types = {^^Ts...};
        using type = [:types[N]:];
    };
}

References

  • Time to Introspect, a talk by Sarthak Sehgal at CppOnline 2026.
  • enum to string C++26 P2996 Reflection Support