C++26 Reflection Practice 1: Basics and Enum-to-String

Tired of using heavy third-party libraries like magic_enum and relfection-cpp that abuse macro hacks and crush compiler template limits just to print an enum name?

With C++26 Static Reflection (P2996), the compiler finally natively understands its own AST. Combining this with C++23 Modules (import std;), we can implement a compile-time, zero-runtime-overhead enum_to_string utility that compiles instantly. Here is the modern stack onboarding using GCC 16 and CMake 4.x on Arch Linux.

The Stack Setup (Arch Linux)

As of mid-2026, combining CMake 4.x’s native module scanning with GCC 16’s reflection requires standard C++26 flags enabled alongside module support.

cmake_minimum_required(VERSION 4.3)

set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "451f2fe2-a8a2-47c3-bc32-94786d8fc91b")

project(reflection_hello)

set(CMAKE_CXX_MODULE_STD ON)
set(CMAKE_CXX_EXTENSIONS OFF)

if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    string(APPEND CMAKE_CXX_FLAGS " -freflection")
endif ()

In order to activate support for import std; in C++23 and newer targets, we need to set the CMake variable CMAKE_EXPERIMENTAL_CXX_IMPORT_STD to the value written in CMake/Help/dev/experimental.rst, just before the project command.

set(target_name reflection_hello)

add_executable(${target_name} main.cpp)

target_sources(${target_name}
        PRIVATE
        FILE_SET CXX_MODULES
        FILES
        toy.ixx)

target_compile_features(${target_name} PRIVATE cxx_std_26)

It is simple to enable C++26 language support for each target individually using target_compile_features command with cxx_std_26 in Modern CMake. All Module Interface Units (.ixx, .cppm, etc.) should be grouped in a standalone FILE_SET CXX_MODULES. Here we get toy.ixx as a demo MIU file.

The Constexpr Table Construction

Utilizing the newly introduced reflexpr operator ^^identifier and splicer operator [:r:], we can have access to the underlying AST data directly – no #include headers, no macros. Just pure C++ Modules and Static Reflection.

import std;

template <typename T>
    requires std::is_enum_v<T>
consteval auto make_enum_to_string_table() noexcept {
    // 1. Reflexpr operator fetches metadata token from AST
    static constexpr auto r       = ^^T; 
    static constexpr auto members = std::define_static_array(std::meta::enumerators_of(r));

    std::size_t index{};
    std::array<std::pair<T, std::string_view>, members.size()> table{};

    // 2. Compile-time loop expansion
    template for (constexpr auto item : members) { 
        table[index].first  = [:item:];            // 3. Splicer operator (meta to value)
        table[index].second = std::meta::identifier_of(item);
        ++index;
    }

    return table;
}

We use a consteval function std::meta::enumerators_of to fetch the enum elements at compile-time, then create a std::array table to store the name-value pairs as a return value to the caller – looks very programmer-friendly and readable. The [:item:] expression is a slicer operator that transforms a reflection to the original value.

Here comes a utility function std::define_static_array defined in <meta> in C++26 Reflection.

template< ranges::input_range R >
consteval std::span<const ranges::range_value_t<R>> define_static_array( R&& r ); // Since C++26

It promotes array to static storage. Let N be static_cast<std::size_t>(ranges::size(r)) if ranges::size(r) is a constant expression, or std::dynamic_extent otherwise. The effect is equivalent to:

using T = ranges::range_value_t<R>;
std::meta::info array = std::meta::reflect_constant_array(r);
if (std::meta::is_array_type(std::meta::type_of(array)))
    return std::span<const T, N>(std::meta::extract<const T*>(array),
                                 std::meta::extent(std::meta::type_of(array)));
else
    return std::span<const T, N>(static_cast<const T*>(nullptr), 0);

The return value of this function is a std::span<T> referencing to the promoted array, with static storage. Other similar functions are std::define_static_string and std::define_static_object. The utility is useful when we need to define some strings at compile-time, instead of using Non-type Template Parameters (NTTP), like auto V .

The template for syntax in C++26, expands a compile-time determinable language construct into its elements, used as a more readable version than std::apply to multiple statements that apply the same logic to multiple expressions.

static constexpr std::array array{1, 2, 3, 4, 5};
static constexpr std::tuple tuple{"Hello", 12345, "World", std::vector{-1, -2, -3}};
static constexpr std::span span{array};

template for (constexpr auto item : array) {
    std::println("Array item: {}", item);
}


template for (constexpr auto item : tuple) {
    // TODO: Process items here.
}

template for (constexpr auto item : span) {
    std::println("Span item: {}", item);
}

// Expands index pack, a replacement to std::make_index_sequence.
template for (constexpr auto index : std::views::iota(0, 10)) {
    std::println("Index: {}", index);
}

The Enum-to-String Implementation

With the clean, modern implementation of static table creation, now we can implement the final enum_to_string function that can be invoked at runtime.

import std;

template <typename T>
    requires std::is_enum_v<T>
constexpr std::string_view enum_to_string(T value) noexcept {
    static constexpr auto table = make_enum_to_string_table<T>();

    template for (constexpr auto item : table) {
        if (item.first == value) {
            return item.second;
        }
    }
    
    return {};
}

int main() {
    enum class task_status { ready, running, pending, stopped };
    
    auto status = task_status::running;
    
    std::println("Status: {}", enum_to_string(status));
}

How it works under the hood:

  • The table = make_enum_to_string_table<T>(); Statement: This generates a constexpr std::array<std::pair<std::string_view, T>> aka. T is an enum type. It’s a lightweight lookup table with static storage.
  • The template for(...) Statement: Unlike a traditional runtime for loop, this unrolls at compile-time. It only duplicates the loop body for every element in the table array, injecting them directly into the generated code.
  • True O(1) Memory Layout: The table is marked static constexpr. The entire string mapping is baked into the binary’s .rodata section during compilation. Zero heap allocations, zero runtime parsing.

Leave a Reply

Your email address will not be published. Required fields are marked *