Почему в 2024 году нам приходится писать каст енума к строке вручную, для каждого кастомного типа нужна своя функция логирования, а биндинги к C++ библиотеке требуют кучу повторяющегося кода?
Если Вы задавались этими, или подобными вопросами, то у меня для вас хорошая новость - скоро эти проблемы будут решены. И что самое приятное - на уровне языка, а не нестандартным фреймворком.
Сегодня рассматриваем пропозалы рефлексии, которые с большОй вероятностью попадут в следующий стандарт - C++26.
Что это вообще такое?
Рефлексия это возможность кода исследовать или даже менять свою структуру. Можно разделить на 2 вида - динамическая и статическая.
Динамическая рефлексия доступна в рантайме (во время выполнения программы). Например питон, где вся информация о типе (методы, данные) хранится в доступной коду структуре данных, благодаря чему можно, например, сериализовать любой объект без дополнительного кода, просто вызвав json.dumps(object)
. Это работает как раз потому, что у функции dumps есть возможность проитерироваться по всем полям данных любого переданного типа.
Статическая работает во время компиляции. Это возможность для кода получить частичный доступ к тому, как программа представлена во внутренних структурах данных компилятора. Это одна из фичей, с которой проще разобраться посмотрев на примеры использования - они будут чуть ниже.
P2996
Основной пропозал, прописывает базу для статической рефлексии. Вводятся два новых оператора и новый хедер <meta> с набором полезных мета функций.
Изменения языка
Новый оператор ^ - да, это переиспользование xor - производит reflection value (reflection/отражение) из типа, переменной, функции, неймспейса и тд. Отражение имеет тип std::meta::info и по сути является ручкой для доступа к внутреннему строению отраженного "объекта".
Splicers - [:R:] - где вместо R вставляется ранее созданное отражение (std::meta::info). Переводит std::meta::info обратно в тип/переменную/функцию/etc.
Изменения библиотеки
Новый тип std::meta::info - для представления отражения.
Метафункции в <meta>, например: members_of - получить список членов какого-то класса, enumerators_of - список констант в переданном енуме, offset_of - отступ переданного субъобъекта (учитывает паддинг), is_noexcept - является ли переданная функция/лямбда noexcept и многое другое.
Использовать все это совсем не сложно, особенно если вы ранее работали с шаблонами.
Примеры (взяты из проползала)
Получение отражения и возврат к изначальному типу
// отражение
constexpr auto r = ^int;
// int x = 42;
typename[:r:] x = 42;
// char c = '*';
typename[:^char:] c = '*';
Обращение к члену класса по имени
class S { int i; int j; };
consteval auto member_named(std::string_view name) {
for (std::meta::info field : nonstatic_data_members_of(^S)) {
if (name_of(field) == name)
return field;
}
}
S s{0, 0};
// s.j = 42;
s.[:member_named("j"):] = 42;
// Ошибка: x не часть класса.
s.[:member_named("x"):] = 0;
Функция member_named принимает имя члена класса. С помощью std::meta::nonstatic_data_members_of запрашивается список имеющихся членов класса, для каждого элемента списка запрашивается его имя с помощью std::meta::name_of. Тот член у которого совпадет имя с переданным в функцию и будет использован.
Шаблонная функция каста енума к строке
template <typename 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::name_of(e));
}
}
return "<unnamed>";
}
enum Color { red, green, blue };
static_assert(enum_to_string(Color::red) == "red");
Функция принимает константу из произвольного енума, отражает его тип, с помощью std::meta::enumerators_of получает список констант этого енума и матчит его с переданной константой. Найденное отражение передается в std::meta::name_of, который возвращает имя константы из обьявления этого енума. Про работу template for чуть ниже.
А так же
В P2996 (см ссылку вначале поста) есть куча других примеров, советую хотя бы пробежаться взглядом. Самый интересный, субъективно, это универсальный форматтер. С помощью него можно будет написать шаблонную функцию, которая сможет переводить любой класс в строку без дополнительного кода. Представьте сколько миллионов строчек кода в мире станут ненужными только за счет этого!
P1306
Expansion statements - представьте, что у вас есть некоторая коллекция объектов разных типов (например, tuple) и вы хотите по ней проитерироваться. Обычный range loop этого не умеет, потому что переменная, с помощью которой происходит итерация, может быть только одного типа. Есть ухищрения вроде std::apply и переводом коллекции в template pack, но это требует дополнительного кода и субъективно довольно костыльно.
Пропозал предлагает новый оператор - template for - он упоминается и в P2996, поскольку этот функционал значительно упрощает написание многих мета функции.
Базовый пример из пропозала
auto tup = std::make_tuple(0, ‘a’, 3.14);
template for (auto elem : tup)
std::cout << elem << std::endl;
Это один из редких случаев в плюсах, когда интуитивно понятно, что делает новая фича. Под капотом все тоже в целом не сложно, но все таки надо упомянуть, что компилятор разворачивает данный цикл примерно в следующее:
{
auto elem = std::get<0>(tup);
std::cout << elem << std::endl;
}
{
auto elem = std::get<1>(tup);
std::cout << elem << std::endl;
}
{
auto elem = std::get<2>(tup);
std::cout << elem << std::endl;
}
template for это не цикл в классическом его понимании, а способ продублировать блок кода для каждого элемента в коллекции, что позволяет элементам быть разного типа.
P3096
Рефлексия параметров функции - фича позволяет получить доступ к информации об аргументах функции. Пример из пропозала, который показывает, как можно вывести все аргументы функции, явно их не перечисляя:
void func(int counter, float factor) {
template for (constexpr auto e : parameters_of(^func))
cout << name_of(e) << ": " << [:e:] << "\n";
}
В предлагаемую пропозалом мета функцию std::meta::parameters_of передается текущая функция. std::meta::parameters_of возвращает вектор с отражениями аргументов функции. std::meta::name_of извлекает имя аргумента из отражения, а [:e:] извлекает значение аргумента в текущем вызове функции. Кстати, этот функционал уже доступен на годболте.
P3096 довольно спорный пропозал - возможно именно поэтому он предлагается отдельно от P2996. Дело в том, что стандарт позволяет объявлять одну и ту же функцию сколько угодно раз, и с какими угодно именами аргументов - главное чтобы совпадали типы. Например:
// file1.h
void func(int value);
// file2.h
void func(int not_a_value);
// file3.cpp
constexpr auto names = meta::parameters_of(^func); // ?
Вопрос, какое имя интового аргумента должна вернуть parameters_of: value или not_a_value? В пропозале представлена аргументация в пользу разных решений, но предлагается следующее: при вызове parameters_of компилятор будет проверять консистентность именования, и если есть несовпадения, то это ошибка компиляции. Таким образом существующий код не ломается, хотя и немного ограничивается область применения новой мета функции.
Новые идеи это круто, но пробовали ли это на практике?
Да! Уже есть две рабочие (но не полные) имплементации. В проде это использовать еще рано, но само их наличие показывает зрелость пропозала.
В EDG - это коммерческий компилятор, поэтому посмотреть код не удастся, но он доступен на годболте
В опенсорсном форке Clang - он так же доступен на годболте, если вам не хочется компилировать кланг самостоятельно : )
Что по принятию в стандарт
Ни один из пропозалов еще не принят комитетом, так что теоретически в C++26 мы можем их не увидеть. Однако наличие рабочих имплементаций и поддержка сообщества позволяют надеяться, что в следующий стандарт рефлексия попадет. В порядке убывания вероятности принятия: P2996, P1306, P3096. Будем следить за следующими собраниями стандартного комитета, ближайшее будет очень скоро - 24 июня в Сент-Луисе.
Заинтересовало?
Если хотите быть в курсе статуса рефлексии и всего остального из мира C++, подписывайтесь на мой телеграм канал.