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

6326758bd3cdec597bd2828a69d47e7c

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

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

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

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

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

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

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

P2996

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

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

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

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

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

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

  2. Метафункции в , например: 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 
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 "";
}

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++, подписывайтесь на мой телеграм канал.

Habrahabr.ru прочитано 2952 раза