метапрограммирование и рефлексияmetaprogramming and reflection

Static Reflection

Возможность на этапе компиляции получить и обработать информацию о структуре программы — типах, полях, функциях, перечислениях — через операторы ^^ (reflect) и [: ... :] (splice), без макросов и генераторов кода.The ability to obtain and process information about a program's structure — types, fields, functions, enumerations — at compile time, via the ^^ (reflect) and [: ... :] (splice) operators, with no macros or code generators involved.

крупнейшее добавление в C++26the largest addition in C++26 частый вопрос: чем отличается от RTTI и от макросов/генераторов кодаcommon question: how it differs from RTTI and from macros/code generators

Что этоWhat it is

Static reflection (P2996) вводит новую сущность языка — std::meta::info, непрозрачное значение, представляющее что угодно в программе: тип, переменную, функцию, поле класса, перечислитель, шаблон. Оператор ^^expr (reflection operator) превращает сущность языка в значение std::meta::info, которое можно передавать, хранить в constexpr-переменных и обрабатывать обычным compile-time кодом. Оператор [: r :] (splice) делает обратное — превращает значение std::meta::info обратно в код: тип, выражение или имя.Static reflection (P2996) introduces a new language entity — std::meta::info, an opaque value representing anything in the program: a type, a variable, a function, a class member, an enumerator, a template. The ^^expr operator (the reflection operator) turns a language entity into a std::meta::info value, which can be passed around, stored in constexpr variables, and processed by ordinary compile-time code. The [: r :] operator (splice) does the reverse — it turns a std::meta::info value back into code: a type, an expression, or a name.

до C++26 — макросы или ручное дублированиеbefore C++26 — macros or manual duplication
// сериализация полей — вручную перечислить каждое поле,// serializing fields — manually list every field,
// либо городить макрос-генератор (BOOST_FUSION_ADAPT_STRUCT и т.п.)// or set up a macro-based generator (BOOST_FUSION_ADAPT_STRUCT etc.)
struct Point { int x, y; };

std::string to_json(const Point& p) {
    return std::format(R"({{"x":{},"y":{}}})", p.x, p.y);  // имена полей вбиты руками// field names hardcoded by hand
}
C++26 — универсальный код, работающий с любой структуройC++26 — generic code that works with any struct
template<typename T>
std::string to_json(const T& obj) {
    std::string out = "{";
    constexpr auto members = std::meta::nonstatic_data_members_of(^^T);
    // цикл по members развёртывается на этапе компиляции// the loop over members unrolls at compile time
    [:expand(members):] >> [&]<auto M>() {
        out += std::format(R"("{}":{},)", std::meta::identifier_of(M), obj.[:M:]);
    };
    out += "}"; return out;
}

Зачем ввелиWhy it was introduced

Рефлексия закрывает нишу, которую десятилетиями латали внешними инструментами и хаками:Reflection fills a gap that had been patched for decades with external tools and hacks:

01

Сериализация, ORM и биндинги требовали внешней кодогенерацииSerialization, ORMs, and bindings required external code generation

Protobuf, Qt (moc), Unreal Engine (UHT) — все они решают одну и ту же задачу: узнать поля и методы класса на этапе сборки — отдельным препроцессором вне стандартного C++, который парсит исходники самостоятельно. Рефлексия переносит эту возможность внутрь языка: компилятор уже знает структуру программы, вопрос лишь в том, чтобы дать программисту доступ к этому знанию.Protobuf, Qt (moc), Unreal Engine (UHT) — they all solve the same problem: learning a class's fields and methods at build time — via a separate preprocessor outside standard C++ that parses the sources on its own. Reflection moves this capability inside the language: the compiler already knows the program's structure, the only question is exposing that knowledge to the programmer.

02

Шаблонное метапрограммирование было мощным, но нечитаемымTemplate metaprogramming was powerful but unreadable

Задачи вроде "перечислить все члены структуры" до C++26 решались через SFINAE, макросы boost::pfr или сторонние библиотеки с трюками вроде structured-bindings-детекции арности — работающие решения существовали, но требовали экспертного уровня владения шаблонами и были хрупкими при обновлении компилятора. Рефлексия даёт для этого прямой, официальный API.Tasks like "enumerate all members of a struct" were, before C++26, solved via SFINAE, boost::pfr-style macros, or third-party libraries using tricks like structured-bindings arity detection — working solutions existed, but required expert-level template mastery and were fragile across compiler upgrades. Reflection provides a direct, official API for this.

03

Единый механизм вместо зоопарка макросов на каждый случайA single mechanism instead of a zoo of macros for every case

Каждая крупная C++-кодовая база в какой-то момент изобретает свой велосипед для интроспекции (свой REFLECT_FIELD-макрос, свой генератор биндингов к скриптовому языку, свой сериализатор). Рефлексия как часть стандарта означает, что эти велосипеды можно заменить на одну переносимую основу, а библиотеки поверх неё (сериализация, ORM, unit-test фреймворки с автогенерацией фикстур) — писать один раз для всех компиляторов.Every large C++ codebase eventually reinvents its own wheel for introspection (its own REFLECT_FIELD macro, its own scripting-language binding generator, its own serializer). Reflection being part of the standard means these wheels can be replaced with one portable foundation, and libraries built on top of it (serialization, ORMs, unit-test frameworks with auto-generated fixtures) can be written once for every compiler.

Как это работает под капотомHow it works under the hood

Ядро API — пространство имён std::meta с функциями, принимающими и возвращающими std::meta::info: members_of(^^T) (все члены), nonstatic_data_members_of(^^T) (только поля-данные), identifier_of(r) (имя как строка), type_of(r), template_of(r). Все эти функции — обычные consteval-функции: вызываются исключительно на этапе компиляции, а их результат — тоже compile-time значение.The core of the API is the std::meta namespace with functions that take and return std::meta::info: members_of(^^T) (all members), nonstatic_data_members_of(^^T) (data members only), identifier_of(r) (the name as a string), type_of(r), template_of(r). All of these are ordinary consteval functions: called exclusively at compile time, with a compile-time result.

struct Point { int x; int y; std::string label; };

constexpr auto refl = ^^Point;                       // reflection самого типа Point// a reflection of the Point type itself
constexpr auto members = std::meta::nonstatic_data_members_of(refl);
static_assert(members.size() == 3);

// splice: превратить reflection обратно в код — здесь в имя типа// splice: turn a reflection back into code — here, into a type name
[:refl:] p{1, 2, "origin"};   // [:refl:] эквивалентно написанию "Point" вручную// [:refl:] is equivalent to writing "Point" by hand

// доступ к полю по reflection через obj.[:member:]// accessing a field by reflection via obj.[:member:]
constexpr auto x_member = members[0];
int value = p.[:x_member:];   // эквивалентно p.x, но имя поля выбрано динамически на этапе компиляции// equivalent to p.x, but the field name was chosen dynamically at compile time

Циклы по спискам reflection-значений выполняются функцией expand() в паре с шаблонной лямбдой — конструкция [:expand(range):] >> template_lambda вызывает лямбду по одному разу на каждый элемент, полностью развёртывая цикл на этапе компиляции (аналог for constexpr, которого в языке нет напрямую).Loops over lists of reflection values are performed via the expand() function paired with a template lambda — the [:expand(range):] >> template_lambda construct invokes the lambda once per element, fully unrolling the loop at compile time (the closest thing to a for constexpr, which the language doesn't have directly).

// перечислить имена и типы всех полей структуры во время компиляции// enumerate the names and types of all fields of a struct at compile time
template<typename T>
consteval void print_layout() {
    [:std::meta::expand(std::meta::nonstatic_data_members_of(^^T)):]
        >> [] <auto M>() {
            std::cout << std::meta::identifier_of(M) << ": "
                      << std::meta::display_string_of(std::meta::type_of(M)) << "\n";
        };
}

Примеры из реального кодаReal-world examples

типичные сценарии использованияtypical usage scenarios
01

Универсальная JSON-сериализация без макросовGeneric JSON serialization with no macros

Одна шаблонная функция сериализует в JSON любую структуру данных, перечисляя её поля через рефлексию — не нужен ни отдельный кодогенератор, ни ручное перечисление полей для каждого нового типа.One template function serializes any data structure to JSON by enumerating its fields via reflection — no separate code generator, no manual field listing for every new type.

template<typename T>
std::string to_json(const T& obj) {
    std::string out = "{";
    bool first = true;
    [:std::meta::expand(std::meta::nonstatic_data_members_of(^^T)):]
        >> [&]<auto M>() {
            if (!first) out += ",";
            out += std::format(R"("{}":"{}")", std::meta::identifier_of(M), obj.[:M:]);
            first = false;
        };
    return out + "}";
}
// to_json(Point{1, 2, "origin"}) -> {"x":"1","y":"2","label":"origin"}// to_json(Point{1, 2, "origin"}) -> {"x":"1","y":"2","label":"origin"}
02

Автоматический operator== по всем полямAutomatic operator== over all fields

Для структур без явно объявленного operator== (и без = default, если поля меняются часто и его пришлось бы поддерживать вручную) — сравнение поле-за-полем, сгенерированное рефлексией, а не написанное руками.For structs without an explicitly declared operator== (and without = default, when fields change often enough that it would need manual upkeep) — a field-by-field comparison generated via reflection instead of hand-written.

template<typename T>
constexpr bool reflective_equals(const T& a, const T& b) {
    bool equal = true;
    [:std::meta::expand(std::meta::nonstatic_data_members_of(^^T)):]
        >> [&]<auto M>() {
            equal = equal && (a.[:M:] == b.[:M:]);
        };
    return equal;
}
03

Именование enum-значений без макроса STRINGIFYNaming enum values without a STRINGIFY macro

Классическая боль C++ — превратить значение перечисления обратно в строку с его именем — раньше решалась макросом, перечисляющим каждый enumerator дважды (в enum и в switch/массиве строк). Рефлексия делает это одной универсальной функцией.A classic C++ pain point — turning an enum value back into a string with its name — used to be solved with a macro listing every enumerator twice (in the enum and in a switch/array of strings). Reflection does it with one generic function.

enum class Color { Red, Green, Blue };

template<typename E>
std::string_view enum_name(E value) {
    constexpr auto enumerators = std::meta::enumerators_of(^^E);
    std::string_view result = "?";
    [:std::meta::expand(enumerators):] >> [&]<auto E_>() {
        if ([:E_:] == value) result = std::meta::identifier_of(E_);
    };
    return result;
}
// enum_name(Color::Green) -> "Green"// enum_name(Color::Green) -> "Green"
04

Компилятор биндингов к скриптовому/RPC слоюA compiler for bindings to a scripting/RPC layer

Регистрация методов класса в системе RPC (или в биндинге к Lua/Python) без отдельного шага генерации кода — рефлексия перечисляет публичные методы, и каждый регистрируется автоматически под своим именем.Registering a class's methods with an RPC system (or a Lua/Python binding) with no separate code-generation step — reflection enumerates the public methods, and each is registered automatically under its own name.

template<typename T>
void register_rpc_methods(RpcServer& server, T& instance) {
    [:std::meta::expand(std::meta::members_of(^^T)):]
        >> [&]<auto M>() {
            if constexpr (std::meta::is_function(M) && std::meta::is_public(M)) {
                server.bind(std::meta::identifier_of(M),
                            [&](auto... args) { return (instance.*[:M:])(args...); });
            }
        };
}

ГраблиGotchas

Это не runtime-рефлексия — std::meta::info не переживает компиляциюThis is not runtime reflection — std::meta::info doesn't survive compilation

В отличие от Java/C#/Python, где рефлексия работает во время выполнения программы (можно узнать тип объекта, полученного динамически, во время работы приложения), C++26 reflection — целиком compile-time механизм. Нельзя получить std::meta::info для типа, который стал известен только в runtime — вся работа с рефлексией должна закончиться до генерации машинного кода, результатом становится обычный сгенерированный C++-код, а не рефлексивные структуры, живущие в бинарнике.Unlike Java/C#/Python, where reflection works at program runtime (you can inspect the type of an object obtained dynamically while the app is running), C++26 reflection is entirely a compile-time mechanism. You cannot obtain a std::meta::info for a type only known at runtime — all reflection work must finish before machine code generation; the end result is ordinary generated C++ code, not reflective structures living in the binary.

expand() — не обычный цикл, а развёртка шаблонаexpand() isn't an ordinary loop — it's a template unrolling

Конструкция [:expand(range):] >> lambda инстанциирует шаблонную лямбду отдельно для каждого элемента диапазона — при большом числе полей (сотни) это означает соответствующее число инстанциаций шаблона, что раздувает время компиляции и размер бинарника аналогично любому другому чрезмерному метапрограммированию. Не стоит применять рефлексию бездумно к структурам с тысячами полей в горячем пути компиляции.The [:expand(range):] >> lambda construct instantiates the template lambda separately for each element of the range — with a large number of fields (hundreds), this means a corresponding number of template instantiations, bloating compile time and binary size just like any other excessive metaprogramming. Reflection shouldn't be applied carelessly to structures with thousands of fields on a compilation hot path.

Отличие от RTTI: полнота информации, но никакой цены в runtimeDifference from RTTI: full information, but zero runtime cost

typeid/dynamic_cast (RTTI) дают ограниченную информацию (имя типа, иерархию наследования для полиморфных типов) ценой рантайм-накладных расходов (vtable, RTTI-таблицы в бинарнике). Static reflection даёт исчерпывающую информацию (все поля, все методы, атрибуты) полностью на этапе компиляции — рантайм-код, который из неё получается, не содержит накладных расходов сверх того, что программист написал бы вручную.typeid/dynamic_cast (RTTI) give limited information (the type's name, the inheritance hierarchy for polymorphic types) at the cost of runtime overhead (a vtable, RTTI tables in the binary). Static reflection gives exhaustive information (every field, every method, attributes) entirely at compile time — the runtime code that comes out of it carries no overhead beyond what a programmer would have written by hand.