Статическая рефлексия обсуждается в грядущем C++26. Wu Yongwei демонстрирует, как применять рефлексию уже сейчас, и показывает примеры того, что может быть станет возможным в C++26.
Статическая рефлексия станет важной частью генерации программы на C++ во время компиляции, как я рассказывал в октябрьском выпуске Overload. Здесь мы детально рассмотрим статическую рефлексию, включая то, как эмулировать её прямо сейчас до того, как она будет добавлена в Стандарт.
Истоки
Многие языки программирования поддерживают рефлексию (например, Python и Java). C++ уступает им.
Сейчас это так, но в C++26 всё, вероятно, изменится. И то, что станет доступно в C++, будет сильно отличаться от того, что есть в таких языках, как Java или Python. Ключевое отличие - слово «статическая».
Andrew Sutton определяет «статическую рефлексию» так:
Статическая рефлексия — это неотъемлемая способность метапрограммы наблюдать свой собственный код и, в какой-то степени, генерировать новый код во время компиляции.
«Время компиляции» — это особенность C++, позволяющая нам делать вещи, невозможные в других языках:
Абстракция без издержек. Как сказал Бьярне Страуструп: «Вы не платите за то, чем не пользуетесь. То, что вы используете, вряд ли закодируете вручную лучше.» Наличие статической рефлексии в языке не сделает вашу программу жирнее или медленнее, если она вам не нужна. Но она будет у вас под рукой, когда она вам понадобится.
Высокая производительность. Благодаря рефлексии во время компиляции можно достичь нераспределённой производительности, недостижимой в таких языках, как Java или Python.
Универсальность как во время компиляции, так и во время выполнения. Информация, доступная во время компиляции, может быть использована программой во время выполнения, но не наоборот. Статическая рефлексия C++ может делать вещи, которые возможны в таких языках, как Java, и есть вещи в С++, которые просто невозможны в других языках.
Что нам нужно от рефлексии
Когда мы говорим о статической рефлексии, чего мы на самом деле желаем? Мы действительно хотим знать то, что видит компилятор, и мы хотим иметь возможность пользоваться этой информацией в коде. Наиболее яркими примерами являются пользовательские типы enum
и struct
. Мы хотим иметь возможность перебирать все константы в перечислении и знать их имена и значения. Мы хотим иметь возможность перебирать все поля структуры и знать их имена и типы. Разумеется, что когда поле является перечислением или структурой, нам также нужна возможность рекурсивной рефлексии. И так далее.
К сожалению, сегодня мы не можем всё это делать с помощью доступных «стандартных» инструментов. Конечно, в некоторых реализациях можно хакнуть часть информации с помощью различных трюков. Я бы предпочёл пользоваться макросами и шаблонными методами для достижения этой цели, так как с ними код несколько аккуратнее, более переносим и более удобен в обслуживании, чем то же самое, но с использованием нестандартных синтаксисов. Конечно, всё это не сравнится с прямой поддержкой будущего стандарта C++.
Несколько слов о технике с макросами
За много лет я собрал коллекцию полезных макросов, начиная с работ Netcan. Ключевые макросы:
GET_ARG_COUNT
: взять количество переменных,GET_ARG_COUNT(a, b, c)
развернётся в 3.REPEAT_ON
: передать аргументы макросу (со счетчиком),REPEAT_ON(func, a, b, c)
станетfunc(0, a) func(1, b) func(2, c)
.PAIR
: удалить первую пару скобок из аргумента,PAIR((long)v1)
станетlong v1
.STRIP
: удалить первую часть в скобках,STRIP((long)v1)
станетv1
.…
Некоторые идеи возникли примерно в начале 2012 года, но тот код Paul Fultz не подходил для реальных программных проектов. Мой текущий код следует считать готовым к использованию, его модификации уже использовались в некоторых крупных приложениях. Он также был протестирован для всех основных компиляторов, включая нестандартный MSVC (поддержка старого MSVC потребовала некоторых усилий). Вы можете найти мои макросы в проекте Mozi с открытым исходным кодом.
Многие считают макросы адом, и их действительно следует избегать, если можно найти лучшие альтернативы, но лично я считаю, что макросы проще понять и поддерживать, чем некоторые шаблонные техники.
Начнём с рефлексии перечислений
Часто желательно знать количество величин определенных в перечислениях, какой целочисленный тип лежит в их основе и их строковые формы. Последние особенное важны для отладки и логирования.
Существующие реализации
Существуют библиотеки, предоставляющие необходимые возможности, например Magic Enum C++ и Better Enums.
Magic Enum C++ требует новейшего компилятора C++17 и работает со стандартной формой определения перечисления. Однако, поскольку он использует методы времени компиляции для поиска значений перечислений, диапазон перечислений ограничен. Кроме того, он плохо уживается с числовыми значениями перечисления, которые не объявлены в определении (например, что-то вроде Color{100}
) — обращение к magic_enum::enum_name
для такого значения даст пустой string_view
. Тем не менее, я рекомендую использовать его, если он подходит вашим потребностям.
Better Enums работает практически с любым компилятором, даже со старыми C++98. Однако он требует применения специальных правил для определения перечисления. Это само по себе уродливо, но приемлемо. Еще уродливее то, что результат не является типом перечисления, и он не может ужиться со значениями, которые вообще не объявлены в определении перечисления — преобразование такого значения в строку вызовет ошибку сегментации…
Моя собственная реализация
Главным образом для того, чтобы лучше понять проблему, я попробовал сам сделать рефлексию перечислений. Я достиг следующего:
Результат генерации кода по-прежнему является перечислением
Обеспечение отображения именованных констант перечисления в их строковые формы с помощью инлайн-переменных
constexpr
Поддержка необходимых операций с использованием перегруженных функций, таких как
to_string
Пример определения enum class
:
DEFINE_ENUM_CLASS(Color, int,
red = 1, green, blue);
Может быть использован так:
cout << to_string(Color::red) << '\n';
cout << to_string(Color{9}) << '\n';
Что выдаст результат:
red
(Color)9
Некоторые детали реализации
Хотя вы можете взять детали реализации из проекта Mozi, я хотел бы показать, что делает DEFINE_ENUM_CLASS
. Его определение:
#define DEFINE_ENUM_CLASS(e, u, ...) \
enum class e : u { __VA_ARGS__ }; \
inline constexpr std::array< \
std::pair<u, std::string_view>, \
GET_ARG_COUNT(__VA_ARGS__)> \
e##_enum_map_{REPEAT_FIRST_ON( \
ENUM_ITEM, e, __VA_ARGS__)}; \
ENUM_FUNCTIONS(e, u)
Вы можете увидеть, что он выполняет три действия:
Определяет стандартный
enum class
Определяет инлайновый
constexpr
-массив, который содержит пары значений базового целочисленного типа (underlying integer) и строковые формы значений, которые генерируются путем применения макросаENUM_ITEM
к значениям перечисленияОбъявляет вспомогательные функции для нового перечисления
enum
После определения Color
выше, он разворачивается на первом уровне до
enum class Color : int { red = 1, green, blue };
inline constexpr std::array<
std::pair<int, std::string_view>, 3>
Color_enum_map_{
ENUM_ITEM(0, Color, red = 1),
ENUM_ITEM(1, Color, green),
ENUM_ITEM(2, Color, blue),
};
ENUM_FUNCTIONS(Color, int)
Полное разворачивание приводит к чему-то вроде
enum class Color : int { red = 1, green, blue };
inline constexpr std::array<
std::pair<int, std::string_view>, 3>
Color_enum_map_{
std::pair{
to_underlying(Color(
(eat_assign<Color>)Color::red = 1)),
remove_equals("red = 1")},
std::pair{
to_underlying(
Color((eat_assign<Color>)Color::green)),
remove_equals("green")},
std::pair{to_underlying(Color((
eat_assign<Color>)Color::blue)),
remove_equals("blue")},
};
inline std::string to_string(Color value)
{
return enum_to_string(to_underlying(value),
"Color",
Color_enum_map_.begin(),
Color_enum_map_.end());
}
Этого должно быть достаточно, чтобы вы увидели основные идеи. Подробности реализации вы можете посмотреть в проекте Mozi, если вам интересно.
Пример рефлексии перечисления в C++26
Этот код должен работать в соответствии с рассматриваемым предложением по статической рефлексии для C++26 P2996.
template <typename E>
requires std::is_enum_v<E>
std::string 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 std::string("(") +
std::meta::identifier_of(^E) + ")" +
std::to_string(to_underlying(value));
}
Он использует следующие новые конструкции для рефлексии:
^E
генерирует информацию для рефлексии перечисленияE
.[:e:]
«встраивает» в код специальный объект рефлексии, который здесь является значением константы перечисления.Специальный цикл
template for
позволяет итерироваться по разнородным объектам во время компиляции.std::meta::enumerators_of
получает все значения перечисления.std::meta::identifier_of
получает идентификатор/имя объекта. Здесь мы используем его дважды, один раз - для имени константы и другой раз - для имени перечисления.
Этот пример делает то же самое, что и мой самодельный to_string
, но без костылей: макросы больше не нужны.
Реализация более раннего предложения P2320, доступная в Compiler Explorer, удобна для демонстрационных целей. Очевидные различия между P2996 и P2320 — это имена функций: enumerators_of
было members_of
, а identifier_of
было name_of
. Существуют и другие компиляторы с поддержкой рефлексии на Godbolt, которые пока недостаточно эффективны, в основном из-за отсутствия поддержки операторов разворачивания. Я написал два разных примера рефлексии enum
, которые работают под P2320:
https://cppx.godbolt.org/z/8rWTcf1KP: Простой пример, который выполняет линейный поиск, как показано выше.
https://cppx.godbolt.org/z/P5Ycdv3xj: Более сложный пример, который собирает строковые формы констант в перечислении и сортирует их, чтобы мы могли использовать двоичный поиск (похоже на то, что я делал в Mozi.)
Как вы можете видеть, не смотря на то, что реализация полной логики все еще не является тривиальной, главное преимущество заключается в том, что мы можем определять перечисления в стандартной форме без ограничений в Magic Enum C++. Информация в рефлексии может быть доступна во время компиляции, но мы можем сохранить её, чтобы получить к ней доступ позже во время выполнения.
Рефлексия для структур
Потребность в рефлексии структур ещё сильнее, чем перечислений. Рефлексия очень полезна при отладке или логировании, а сериализация и десериализация становятся простыми, когда есть рефлексия.
Существующие реализации
Я знаком в двумя существующими реализациями рефлексии.
…библиотека на C++14 для очень простой рефлексии, которая даёт вам доступ к элементам структуры по индексу и предоставляет другие методы, подобные тем, что есть для
std::tuple
, для пользовательских типов без использования каких-либо макросов или повторяющегося кода.
Она проста в использовании. Поддерживает общие операции, такие как итерация, сравнение и вывод. Однако из-за отсутствия статической рефлексии она не имеет возможности доступа к именам полей.
Struct_pack — это «очень простая в использовании, высокопроизводительная библиотека сериализации.» Она требует C++17 и специализируется на сериализации/десериализации. В целом, она не предназначена для рефлексии, и вы не можете использовать её для собственных сценариев рефлексии (без серьёзных хаков).
Хотя это и не настоящая реализация, самый ранний известный мне код для рефлексии структур принадлежит Paul Fultz. Современные возможности времени компиляции еще не были готовы в 2012 году, поэтому, хотя основные идеи были похожи, Netcan и я не заимствовали много кода оттуда.
Моя собственная реализация
У меня есть собственная реализация для рефлексии структур, которая не имеет ограничений Boost.PFR, но использует макросы. Однако, как только статическая рефлексия будет стандартизирована, большую часть кода можно будет адаптировать к стандартному C++.
Основной подход состоит из:
Использование макросов для генерации кода, чтобы результирующий тип действительно представлял собой структуру предполагаемого размера (не больше!)
Генерирование вложенных типов и статических
constexpr
полей, которые предоставляют необходимую информациюПредоставление независимых/внешних шаблонов функций для общих операций.
Вот пример. Предположим, у нас есть следующие определения структур:
DEFINE_STRUCT(
Point,
(double)x,
(double)y
);
DEFINE_STRUCT(
Rect,
(Point)p1,
(Point)p2,
(uint32_t)color
);
Тогда мы можем инициализировать такие структуры как обычно:
Rect rect{
{1.2, 3.4},
{5.6, 7.8},
12345678
};
Легко выводить на печать
print(data);
И получать
{
p1: {
x: 1.2,
y: 3.4
},
p2: {
x: 5.6,
y: 7.8
},
color: 12345678
}
Сценарий использования: копирование одноименных полей
Детали реализации могут быть неинтересными, но у нас есть более интересные сценарии использования. Одна вещь, которую я реализовал, — это копирование пересекающихся полей.
Предположим, что даны следующие определения структур (обратите внимание, что v2
и v4
имеют разные типы в S1
и S2
):
DEFINE_STRUCT(S1,
(uint16_t)v1,
(uint16_t)v2,
(uint32_t)v3,
(uint32_t)v4,
(string)msg
);
DEFINE_STRUCT(S2,
(int)v2,
(long)v4
);
S1 s1{ /* ... */};
// ...
S2 s2;
Затем, такое выражение будет делать правильную вещь:
copy_same_name_fields(s1, s2);
И это будет сделано с максимально возможной эффективностью, эквивалентной s2.v2 = s1.v2; s2.v4 = s1.v4;
. Я проверил его ассемблерный код x86-64, сгенерированный компилятором, который получился таким:
movzx eax, WORD PTR s1[rip+2]
mov DWORD PTR s2[rip], eax
mov eax, DWORD PTR s1[rip+8]
mov QWORD PTR s2[rip+8], rax
Я не думаю, что Java или Python когда-либо смогут сделать что-то подобное!
Если это не выглядит полезным, просто подумайте о больших записях в базах данных. Представьте, что у нас есть контейнер больших объектов BookInfo
, и мы хотим сделать что-то вроде SELECT name, publish_year WHERE author_id = …
на SQL. Код мог бы быть таким:
DEFINE_STRUCT(
BookInfoNameYear,
(string)name,
(int)publish_year
);
BookInfoNameYear record{};
vector<BookInfoNameYear> result;
Container<BookInfo> container;
while (/* ... */) {
auto it = container.find(/* ... */);
// ...
copy_same_name_fields(*it, record);
result.push_back(record);
}
Этот код намного проще, не так ли, при этом, столь же эффективен, как ручное копирование нужных полей. Преимущество особенно очевидно, когда таких полей много.
Я видел подобное копирование десятков полей в реальном коде, часто сопровождаемое сериализацией (для отправки информации по сети), эту тему я рассмотрю отдельно.
Технические детали
DEFINE_STRUCT
определена так:
#define DEFINE_STRUCT(st, ...) \
struct st { \
using is_reflected = void; \
template <typename, size_t> \
struct _field; \
static constexpr size_t _size = \
GET_ARG_COUNT(__VA_ARGS__); \
REPEAT_ON(FIELD, __VA_ARGS__) \
}
s2
из примера выше развернётся в:
int v2;
template <typename T>
struct _field<T, 0> {
using type = decltype(decay_t<T>::v2);
static constexpr auto name = CTS_STRING(v2);
constexpr explicit _field(T&& obj)
: obj_(std::forward<T>(obj)) {}
constexpr decltype(auto) value()
{ return (std::forward<T>(obj_).v2); }
T&& obj_;
};
Я оставляю CTS_STRING(v2)
неразвёрнутым, потому что у него есть два возможных определения в зависимости от среды. Пока что вы можете думать о нём просто как о "v2" с некоторой дополнительной магией (которую требует copy_same_name_fields
.)
Если у вас есть obj
типа S2
, вы можете получить доступ к его полям, используя номера их полей: _field(obj).value()
— это в точности obj.v2
(с правильной категорией значения), а S2::_field::type
— это тип obj.v2
(который является int
). С помощью выражений свёртки теперь возможны более сложные вещи, такие как итерация полей во время компиляции, как показано ниже:
template <size_t I, typename T>
constexpr decltype(auto) get(T&& obj)
{
using DT = decay_t<T>;
static_assert(I < DT::_size,
"Index to get is out of range");
return typename DT::template _field<T, I>(
std::forward<T>(obj))
.value();
}
template <typename T, typename F, size_t... Is>
constexpr void
for_each_impl(T&& obj, F&& f,
std::index_sequence<Is...>)
{
using DT = decay_t<T>;
(void(std::forward<F>(f)(
index_t<Is>{},
DT::template _field<T, Is>::name,
get<Is>(std::forward<T>(obj)))),
...);
}
template <typename T, typename F>
constexpr void for_each(T&& obj, F&& f)
{
using DT = decay_t<T>;
for_each_impl(
std::forward<T>(obj), std::forward<F>(f),
std::make_index_sequence<DT::_size>{});
}
Теперь такой вызов функции как for_each(obj, f)
будет эквивалентен:
f(0, S2::_field<S2&, 0>::name, get<0>(obj));
f(1, S2::_field<S2&, 1>::name, get<1>(obj));
Такие возможности, как for_each
, необходимы для реализации пользовательских инструментов подобных печати и сериализации.
Пример рефлексии структуры в C++26
Как и в случае с рефлексией перечисления, мы сможем обойтись без макросов, когда появится статическая рефлексия в C++26. Демонстрационная реализация print
(слегка изменённая по сравнению с примером из C++ Compile-Time Programming для соответствия обновлённой версии предложения P2996):
template <typename T>
void print(const T& obj, ostream& os = cout,
std::string_view name = "",
int depth = 0)
{
if constexpr (is_class_v<T>) {
os << indent(depth) << name
<< (name != "" ? ": {\n" : "{\n");
template for (constexpr meta::info member :
meta::nonstatic_data_members_of(^T)) {
print(obj.[:member:], os,
meta::identifier_of(member),
depth + 1);
}
os << indent(depth) << "}"
<< (depth == 0 ? "\n" : ",\n");
} else {
os << indent(depth) << name << ": " << obj
<< ",\n";
}
}
Уже зная, что такое ^T
и [:member:]
, показанный код - достаточно простой. Вот некоторые ключевые моменты (обратите внимание, что синтаксис может быть изменён до окончательного принятия в C++26):
^T
— это предлагаемый синтаксис для получения объекта рефлексии (неопределённого типа) во время компиляции.[:expr:]
— это обращение объекта рефлексии обратно в тип C++ или выражение и встраивание его в код;[:^T:]
даёт намT
в коде.template for
— это цикл времени компиляции для итерации по объектам во время компиляции, предложение P1306R2, устраняющий необходимость в универсальных лямбда‑выражениях иfor_each
.-
Пространство имён
std::meta
предоставляет инструменты для работы с объектом рефлексии во время компиляции:info
— обобщённый объект рефлексии.members_of
извлекает вектор рефлексий всех полей типа или пространства имён.nonstatic_data_members_of
извлекает нестатические поля данных.identifier_of
получает имя рефлексируемого объекта.
Мы можем увидеть рабочий код для предложений P2320 (https://cppx.godbolt.org/z/G3EcvhKxK) и P2996 с обходным решением с помощью оператора разворачивания expand
(https://godbolt.org/z/631Tebb91).
Несколько слов о Mozi
Mozi — это проект с открытым исходным кодом, который я начал в конце 2023 года, в основном для экспериментов со статической рефлексией на основе макросов. Я реализовал общее сравнение, копирование, печать и сериализацию/десериализацию. Реализован сценарий сериализации под названием net_pack
, который включает полностью автоматическую замену порядка байтов и подходит для работы с сетевыми датаграммами. Для поддержки битовых полей по сети предусмотрен специальный тип bit_field
.
Я рассматриваю это как демонстрацию некоторых интересных вещей, которые возможны со статической рефлексией. То, что в настоящее время реализуется с помощью макросов, будет возможно со статической рефлексией в C++26, только это будет проще, как для программиста, так и для пользователя.
Комментарии (23)
kovserg
01.01.2025 23:11А какой смысл было вводить новый синтаксис типа баян с крышечкой [:^III:] ? Почему нельзя было использовать слова? (sizeof, elementsof, ...) Или слов не хватает для такого безобразия?
Более того что мешает добавить новые макросы типа #include но наоборот что бы генерировало файл с информацией о запрошенных элементах программы (типах шаблонах и т.п.) аля #export_details "info1.txt" selector(args,...) и уже на основе этой информации делать анализ или генерацию нового кода для программы. Нафига постоянно пытаются добавить побольше инопланетного синтаксиса. И вместо единообразия сделать всё максимально разношерстным? Вместо разделения разных задач на независимые более простые, смешивают всё в кучу, порождая ненужную сложность.sergio_nsk Автор
01.01.2025 23:11Никто не заставляет пользоваться. В начале написано - если это есть, и ты этим не пользуешься, то ничего для тебя не изменится.
Причешут. Лямбды, constexpr, списки инициализации причёсывали в три итерации после их релиза. Сейчас причёсывают модули, ranges, fmt уже вторую итерацию. Потом к ним добавится или нет статическая рефлексия. А сейчас там на этапе предложения вообще всё может поменяеться, не в первый раз.
Playa
01.01.2025 23:11Такое чувство, что комитет покусал кто-то из Microsoft, потому что там тоже релизят малоюзабельное нечто и потом много лет допиливают.
geornit25
01.01.2025 23:11конечно хорошо, что это появится хоть в каком то виде. но вот тоже непонятно это желание побольше всяких непонятных интуитивно обозначений использовать. почему не просто typeof(T)? также любопытно как это планируется в C++/CLI реализовать, где крышечка уже занята под reference.
sergio_nsk Автор
01.01.2025 23:11Там
T^
никак не конфликтует с^T
. Но всё идёт к тому, что будет^^T
. Мне уже пришлось адоптировать последний авторский код на Godbolt под^^
.
sergegers
01.01.2025 23:11typeof() занят GCC, кажется
geornit25
01.01.2025 23:11по хорошему, выпилить бы его оттуда в пользу decltype. а так вообще вопрос интересный: должен ли стандарт языка вообще оглядываться на расширения компиляторов и диалекты этого языка? вон выше автор поста ссылку привел на дискуссию по этой теме: https://habr.com/ru/articles/870750/comments/#comment_27735940
alexac
01.01.2025 23:11Комитет C++ очень печется об обратной совместимости. Введение любого нового ключевого слова, которого никогда не было в языке — поломка обратной совместимости, потому что до введения этого ключевого слова кто-то мог использовать его в качестве имени переменной/функции/типа. поэтому любое новое ключевое слово вводится с очень большим трудом.
Я не знаю, как в C++20 пролезли char8_t, co_await, co_yield, co_return, concept, requires, consteval, constinit, import, & module. Это просто праздник какой-то. Не думаю, что в ближайшее время мы увидем новые ключевые слова.
AndriyS
01.01.2025 23:11Термин "Статическая рефлексия " дан максимально непонятно. Определение звучит как некая магия. В чем статичность? В чем отличие от RTTI Delphi ? или C#? Ну или раз автор не знает про рефлексию в этих языках, а упоминает Python и Java и говорит, что будет не так как там, то хотя бы уже хоть немного надо сказать в чем же разница. Информацию о типах будут генерировать не для всех доступных, а только для какой то части ? Вроде того что типов, для которых на этапе компиляции понятно, что будет использоваться информация о типах? Или для всех которые потенциально в generic могут быть использованы, тогда это тоже самое, что RTTI, только давайте назовем по-другому, чтобы по непонятнее было.
m0xf
01.01.2025 23:11В итоговый исполняемый файл попадает только та информация, которая используется на этапе выполнения. Причём, эту информацию можно предварительно обработать на этапе компиляции, и использовать уже результат обработки. В других языках RTTI всегда хранит полную информацию.
HemulGM
01.01.2025 23:11Хранит, потому что может быть не известно, что нужно будет, а что нет на момент сборки. Например, в программе может быть "точка доступа" к ядру программы через UI и что запросит пользователь и в каком виде не известно.
ahdenchik
01.01.2025 23:11Сейчас это так, но в C++26 всё, вероятно, изменится. И то, что станет доступно в C++, будет сильно отличаться от того, что есть в таких языках, как Java или Python. Ключевое отличие - слово «статическая».
Не могу не отметить что это уже почти догоняет реализацию рефлексии в таких языках как D
buldo
01.01.2025 23:11Пример с копированием по именам полей...
Боже, чую в C++ появится свой automapper, и лет через 10 будут обсуждать является ли он плохой практикой.
sbars
01.01.2025 23:11Не пишу на C++, но в примере с копированием структур, копирование в цикле в один и тот же объект выглядит страшновато
Altren
01.01.2025 23:11Есть замечательная библиотека https://github.com/getml/reflect-cpp, которая реализует статическую рефлексию на C++20 через std::source_location
Без макросов, работает с enum'ами, классами.
Пример рабочего кода:#include <rfl/json.hpp> #include <rfl.hpp> struct MyStruct { std::string field1; int someInteger; std::string field2; }; int main() { MyStruct data{"a", 123, "b"}; std::cout << rfl::json::write(homer); return 0; } // output {"field1":"a","someInteger":123,"field2":"b"}
dyadyaSerezha
Все это хорошо, но...
Невозможно использовать статику в библиотеках, которые работают с заранее неизвестными классами.
Ну и как всегда, в С++ это выглядит как чрезмерно сложный код.
KivApple
На статической рефлексии всегда можно построить динамическую - генерировать с её помощью структуры описатели типов доступные в рантайме (например, для структуры её размер, массив имен и смещений всех полей, примитивные типы полей или указатели на такие же описатели сложных типов, указатели на функции инициализации, копирования и удаления и т. д. - с помощью этой информации можно работать со структурой неизвестной на этапе компиляции).
Разумеется, чтобы это работало между библиотеками, должно быть единогласие, что надо генерировать. Какая-то общая библиотека, шаблон из которой надо инстанциировать для всех своих структур нуждающихся в рефлексии, а он уже генерирует и регистрирует метаинформацию.
До C++26 это было возможно только требуя от клиентского кода описывать структуры всякими нестандартными образами на макросах и т п. Теперь можно будет инстанциировать один шаблон (и не обязательно в том же файле, где тип описан), возможно, отнаследоваться от специального класса с виртуальным методом возвращающим метаинформацию.
ptr128
Динамическая рефлексия, это, например, когда из IR, можно скомпилировать код с инструкциями AVX-512, если они поддерживаются. Или без них, если не поддерживаются. То есть, в зависимости от хоста, на котором k8s поднял pod, получать разный код с разными инструкциями CPU. Пока что сам C++ это никак не поддерживает.
dyadyaSerezha
Звучит не как рефлексия. А что такое IR (infra red)?
ddruganov
Intermediate representation
dyadyaSerezha
Тогда это вообще не динамическая рефлексия, а обычная компиляция для конкретного железа.
dyadyaSerezha
Вы удивитесь, но это называется "стандарт языка".