Почему в 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++, подписывайтесь на мой телеграм канал.
ссылка на оригинал статьи https://habr.com/ru/articles/824840/
Добавить комментарий