Как стать автором
Обновить

Нативная рефлексия в C++ уже близко

Уровень сложностиСредний
Время на прочтение5 мин
Количество просмотров13K

Почему в 2024 году нам приходится писать каст енума к строке вручную, для каждого кастомного типа нужна своя функция логирования, а биндинги к C++ библиотеке требуют кучу повторяющегося кода?

Если Вы задавались этими, или подобными вопросами, то у меня для вас хорошая новость - скоро эти проблемы будут решены. И что самое приятное - на уровне языка, а не нестандартным фреймворком.

Сегодня рассматриваем пропозалы рефлексии, которые с большОй вероятностью попадут в следующий стандарт - C++26.

Что это вообще такое?

Рефлексия это возможность кода исследовать или даже менять свою структуру. Можно разделить на 2 вида - динамическая и статическая.

Динамическая рефлексия доступна в рантайме (во время выполнения программы). Например питон, где вся информация о типе (методы, данные) хранится в доступной коду структуре данных, благодаря чему можно, например, сериализовать любой объект без дополнительного кода, просто вызвав json.dumps(object). Это работает как раз потому, что у функции dumps есть возможность проитерироваться по всем полям данных любого переданного типа.

Статическая работает во время компиляции. Это возможность для кода получить частичный доступ к тому, как программа представлена во внутренних структурах данных компилятора. Это одна из фичей, с которой проще разобраться посмотрев на примеры использования - они будут чуть ниже.

P2996

Основной пропозал, прописывает базу для статической рефлексии. Вводятся два новых оператора и новый хедер <meta> с набором полезных мета функций.

Изменения языка

  1. Новый оператор ^ - да, это переиспользование xor - производит reflection value (reflection/отражение) из типа, переменной, функции, неймспейса и тд. Отражение имеет тип std::meta::info и по сути является ручкой для доступа к внутреннему строению отраженного "объекта".

  2. Splicers - [:R:] - где вместо R вставляется ранее созданное отражение (std::meta::info). Переводит std::meta::info обратно в тип/переменную/функцию/etc.

Изменения библиотеки

  1. Новый тип std::meta::info - для представления отражения.

  2. Метафункции в <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 компилятор будет проверять консистентность именования, и если есть несовпадения, то это ошибка компиляции. Таким образом существующий код не ломается, хотя и немного ограничивается область применения новой мета функции.

Новые идеи это круто, но пробовали ли это на практике?

Да! Уже есть две рабочие (но не полные) имплементации. В проде это использовать еще рано, но само их наличие показывает зрелость пропозала.

  1. В EDG - это коммерческий компилятор, поэтому посмотреть код не удастся, но он доступен на годболте

  2. В опенсорсном форке Clang - он так же доступен на годболте, если вам не хочется компилировать кланг самостоятельно : )

Что по принятию в стандарт

Ни один из пропозалов еще не принят комитетом, так что теоретически в C++26 мы можем их не увидеть. Однако наличие рабочих имплементаций и поддержка сообщества позволяют надеяться, что в следующий стандарт рефлексия попадет. В порядке убывания вероятности принятия: P2996, P1306, P3096. Будем следить за следующими собраниями стандартного комитета, ближайшее будет очень скоро - 24 июня в Сент-Луисе.

Заинтересовало?

Если хотите быть в курсе статуса рефлексии и всего остального из мира C++, подписывайтесь на мой телеграм канал.

Теги:
Хабы:
Всего голосов 23: ↑23 и ↓0+27
Комментарии72

Публикации

Истории

Работа

Программист C++
147 вакансий
QT разработчик
12 вакансий

Ближайшие события

19 августа – 20 октября
RuCode.Финал. Чемпионат по алгоритмическому программированию и ИИ
МоскваНижний НовгородЕкатеринбургСтавропольНовосибрискКалининградПермьВладивостокЧитаКраснорскТомскИжевскПетрозаводскКазаньКурскТюменьВолгоградУфаМурманскБишкекСочиУльяновскСаратовИркутскДолгопрудныйОнлайн
24 – 25 октября
One Day Offer для AQA Engineer и Developers
Онлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
26 октября
ProIT Network Fest
Санкт-Петербург
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань