Почему в 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++, подписывайтесь на мой телеграм канал.
Комментарии (56)
Yura_PST
26.06.2024 15:26Первое впечатление от рефлексии Java после C++ - детям дали два фломастера и они вообще всё разрисовали.
Интелектуальная нагрузка для написания геттеров и сеттеров нулевая. Разобраться как это делать с помощью рефлексии - минимум пол дня. И если в классе много геттеров и сетеров - проблема в декомпозиции.
В общем, вместо реально важдных дел (декомпозиция, рефакторинг, документация) - рефлексия, автогенерация и енамы.
Anarchist
26.06.2024 15:26+2Рефлексия имеет ещё важное применение: сериализация, передача и удаленное выполнение функций. Этим живёт, например, спарк. Если рефлексия появится в низкоуровневых языках, jvm, наверное, можно постепенно начинать закапывать.
dalerank
26.06.2024 15:26Не-не-не, не надо такого Щастья, на одном из проектов использовался кастомный кланг с рефлексией (такой вот заскок был у архитекта системы, причем он сам половину и написал), люди такую дичь начинали делать с использованием этого механизма, то массив перебирают опираясь на наличей полей в объекте, а ты потом - "...ля куда делась половина выборки?" то начинают цеплять выхлоп рефлексии в метаданные к объекту. Не говоря уже что самая рефлексия в той реализации была очень и очень дорогая, на пустом месте просадка перфа на порядок была. В итоге это все умирало на ревью, и имело крайне узкое место применения, вроде проверок на парсинге ресурсов или уровня. Не относится к пропозалу, но изза хотелок архитекта, приходилось еще и держать двух инженеров на саппорте собственной версии компилятора и поддержке компиляции на обычных
Lainhard
26.06.2024 15:26+6Возможно лучшая новость по плюсам за последние N лет. Очень давно жду этого, в свое время даже трогал форкнутый кланг с рефлексией.
Обычно злюсь, когда авторы дают ссылку на телегу, вне зависимости от ценности статьи, но если обещаете там побольше постить про рефлексию, буду рад подписаться.
krestovii_podhod Автор
26.06.2024 15:26Обещаю что будет пост про P3294 и апдейты по стандартизации рефлексии)
DistortNeo
26.06.2024 15:26+3Поизучал proposal. Не увидел, как это будет дружить с атрибутами - без них та же серализация в json как-то мало смысла имеет.
P.S. Как-то прям ну очень некрасиво в плане синтаксиса. Хотя, чего ещё ожидать, когда язык перегружен функционалом.
krestovii_podhod Автор
26.06.2024 15:26+1^ как по мне выглядит неплохо, особенно учитывая редкость использования xor, но [::] пока кажется чужеродным плюсовому коду. Возможно есть технические причины, почему нужен был синтаксис "оборачивающий" отражение вместо обычного унарного оператора (как, например, ^) - тут не знаю. Но если причина в этом, то дальнейшая логика понятна, потому что все скобочки то или иное значение в языке уже имеют
krestovii_podhod Автор
26.06.2024 15:26+2Речь об [[этих]] атрибутах? Есть отдельный пропозал P1887, но по статусу не знаю
Biga
26.06.2024 15:26+1Когда это появится в компиляторах, многие наконец выдохнут с облегчением. И со спокойным сердцем уйдут на пенсию. (ирония)
Porohovnik
26.06.2024 15:26+3auto tup = std::make_tuple(0, ‘a’, 3.14); template for (auto elem : tup) std::cout << elem << std::endl;
Ух, как это прекрасно, лаконично, и можно выкинуть столько костылей, жаль что на C++26 явно перейдут только лет через 6-8 везде, но будущие всё больше становится светлым.
buldo
26.06.2024 15:26+5О, поздравляю коллег плюсовиков.
Если комитет этого не примет, то я окончательно поверю, что это просто сборище дедов, которые уничтожают язык и самоутверждаются на этом.
kovserg
26.06.2024 15:26нам приходится писать каст енума к строке вручную, для каждого кастомного типа нужна своя функция логирования, а биндинги к C++ библиотеке требуют кучу повторяющегося кода?
Скорее всего просто язык для этого не подходящий? Не?
у меня для вас хорошая новость - скоро эти проблемы будут решены
Зато эти решения создадут в два раза больше новых проблем.
Рефлексия это возможность кода исследовать или даже менять свою структуру
А можно собрать все enum-ы каким-нибудь селектором и сформировать новый enum?
Динамическая рефлексия доступна в рантайме
А динамические библиотеки тоже можно будет подгрузить и исследовать?
krestovii_podhod Автор
26.06.2024 15:26+1А можно собрать все enum-ы каким-нибудь селектором и сформировать новый enum?
В основном пропозале возможности объявлять новый енум нету, насколько я знаю
А динамические библиотеки тоже можно будет подгрузить и исследовать?
Рефлексия предлагается строго на этапе компиляции, поэтому нет
yatanai
26.06.2024 15:26+1Думать вредно! Но наличие статик рефлексии сильно упростит рефлексию динамическую. Мой фреймворк где можно делать удалённый вызов процедур, который сейчас работает с помощью отдельного кодогенератора, наконец-то можно будет упростить.
9241304
26.06.2024 15:26+1Каст энума уже работает, без рефлексию. На всякий случай напоминаю)
krestovii_podhod Автор
26.06.2024 15:26+3Знаю про каст к инту, а к строке как?)
9241304
26.06.2024 15:26+1конкретно энамы https://github.com/Neargye/magic_enum
+структуры https://github.com/getml/reflect-cpp
насколько помню, там один и тот же принцип. для больших энамов надо немного подкостыливать+медленнее будет компилиться
UranusExplorer
26.06.2024 15:26структуры https://github.com/getml/reflect-cpp
Boost.PFR (он header-only и на самом деле не требует остального boost) умеет почти то же самое (только без имён полей) для C++17.
Так на случай если кто-то застрял на 17-ой версии стандарта и не может пока перейти на 20-ую.
9241304
26.06.2024 15:26когда кто-то говорит, что буст умеет то же самое, я конечно очень рад (он вообще умеет многое, что скоро появится в стандарте, и в целом отличная либа), но уже одно его название отпугивает )
UranusExplorer
26.06.2024 15:26Ну я специально отметил, что PFR - маленький и header-only, от буста не зависит, и есть даже его версия без бустовского неймспейса.
Anton_Menshov
26.06.2024 15:26Magic enum штука хорошая, пока не упираешься в ее ограничения. Наиболее серьезными для меня были: 1) зависимость от компилятора (разрабатывал приложение которое необходимо было компилировать несколькими компиляторами, включая полуэкзотику (хоть и с С++17, Intel Compiler если интересно); 2) проблемы с
enum
в шаблонах. Часть уже поправлена, однако иногда требуются танцы с бубном, чтобы затарахтело.В итоге, на проекте от
magic_enum
было решено отказаться в пользу к полуавтоматическому (вставляем руками с помощью утилиты там где надо) касту к строке.9241304
26.06.2024 15:26Потому и ждём рефлексию. Но зачем же полуавтоматика, если элементарные декларации с помощью макросов тоже работают?
KuHeT
26.06.2024 15:26+3Около года назад впервые попробовал Java и всё это время писал бэкэнд чисто на ней - теперь смотреть больно на плюсы. Хотя раньше и сильно любил их
Здорово, что может появиться рефлексия в ближайшее десятилетие (давайте будем реалистами), хоть и с таким пугающим синтаксисом
9241304
26.06.2024 15:26больно потом переписывать бэкенд с явы на го. )))
а плюсы даже с такой рефлексией будут норм
firehacker
26.06.2024 15:26Господи... Во что они превращают C++...
9241304
26.06.2024 15:26+1вам по прежнему никто не запрещает писать в сишном стиле без использования стандартной библиотеки и даже классов )
UranusExplorer
26.06.2024 15:26C++ наконец-то потихоньку превращается в приличный язык программирования повернутый к программисту не задницей с кучей костылей, а хоть немного лицом, при этом продолжая придерживаться концепции don't pay for what you don't use. Отлично же.
slonopotamus
26.06.2024 15:26Ну ок, допустим в компайл-тайме мы теперь умеем из енума сделать строку. Хотя могли и до этого. А с рантаймом что делать?
mayorovp
26.06.2024 15:26+1А что мешает-то вызвать ту же enum_to_string в рантайме?
slonopotamus
26.06.2024 15:26У меня вот из этих предложений сложилось впечатление что в рантайме кина не будет:
Динамическая рефлексия доступна в рантайме
Плюс
Статическая работает во время компиляции.
Плюс
Основной пропозал, прописывает базу для статической рефлексии.
mayorovp
26.06.2024 15:26+1Во время компиляции с помощью рефлексии генерируется функция enum_to_string. В рантайме она вызывается.
krestovii_podhod Автор
26.06.2024 15:26+1Динамической рефлексии (см фичи питона) действительно не будет, но для каста енума она не нужна. Сама рефлексия отработает во время компиляции, по сути сгенерировав функцию с кучей if-ов. В рантайме это будет обычной функцией
AndriyS
26.06.2024 15:26+2В Delphi активно пользуюсь RTTI (рефлексией). Кроме сериализации еще одно применение в моем случае - построение юзер интерфейса в рантайме. Интерфейс для редактирования неких данных описываемых структурами/классами. В основном такие прогрммы нужны для тестировния, но без RTTI моя работа была бы адом :). Надеюсь все proposals будут приняты. Жду рефлексию в С++ уже давно.
ImagineTables
26.06.2024 15:26Напомните, что хорошего прикладного можно запилить с помощью статической рефлексии?
Для динамической, например, сходу можно вспомнить универсальный компонент Object Inspector.
mayorovp
26.06.2024 15:26+4Сериализатор. ORM. DI фреймворк. Биндинг к другому языку. Реализацию IDispatch.
Ну или аналог динамической рефлексии, а потом тот же Object Inspector.
ImagineTables
26.06.2024 15:26Реализацию IDispatch.
Боюсь, я несколько подзабыл такие материи, как интеграция с Visual Basic (для этого же нужны диспинтерфейсы?), поэтому поправьте, если что-то не так.
Проблема, как я её помню, была в следующем. Руки чесались сделать макрос, в который бы можно было передать имя метода, и который бы:
Добавлял нативную декларацию в кокласс (для дуальности);
Подклеивал (
##
) к переданному имени кавычки и добавлял его в карту id-имя-адрес.
И никаким каком сделать это было нельзя, потому что 1 и 2 относятся к разным скоупам (а именно: 1) декларация кокласса, 2) имплементация конструктора).
Соответственно, когда я этим занимался, я делал фыр-фыр по поводу тупейшего текстуального препроцессора, доставшегося от мохнатого Си, и мечтал о настоящих синтаксических макросах для качественного метапрограммирования. Которые разруливали бы это всё.
Макросов, как известно, нам не завезли. А что даёт в этом вопросе статическая рефлексия? То, что строку с именем метода можно вытащить, и всё? Или что-то большее? Если делать в лоб универсальную библиотечную реализацию
Invoke
, там же будут всякие спецэффекты типа нежелательного exposing'а наружуAddRef
/Release
/QI
.mayorovp
26.06.2024 15:26Макросов, как известно, нам не завезли. А что даёт в этом вопросе статическая рефлексия? То, что строку с именем метода можно вытащить, и всё?
Можно получить список методов (и их имена, разумеется).То есть никаких больше явных карт - карту соберёт компилятор.
ImagineTables
26.06.2024 15:26Вы про это? Или я чего-то не понимаю?
Если делать в лоб универсальную библиотечную реализацию Invoke, там же будут всякие спецэффекты типа нежелательного exposing'а наружу AddRef/Release/QI.
mayorovp
26.06.2024 15:26Не вижу причин почему в универсальной реализации он более нежелателен чем при использовании тех карт id-имя-адрес
ImagineTables
26.06.2024 15:26Насколько я понимаю, такой exposing для дуальных диспинтерфейсов не «нежелателен», а недопустим, в соответствии со спецификацией COM. (Потому что в VB есть GC, который должен единолично управлять вызовами счётчика ссылок).
Ну, это совсем неинтересно. Я думал, ответ будет из области фильтрации по атрибутам, а так…
mayorovp
26.06.2024 15:26Не помню ничего такого в спецификации COM.
ImagineTables
26.06.2024 15:26Здесь есть кто-то ещё, кто помнит COM? Объясните, пожалуйста, что вы думаете о передаче метода для ручного управления памятью (
Release
) в язык с автоматическим управлением памятью (Visual Basic).А то я видел и такое:
while (pUnk->Release());
и автор тоже утверждал, что это не нарушает комовских спецификаций.
mayorovp
26.06.2024 15:26Погодите, а причём тут этот цикл и что вообще есть "передача метода"?
Вы всё это время пытались меня убедить в том, что методы IUnknown не должны вызываться через IDispatch::Invoke? Ну да, это очевидно. А в чём проблема исключить эти методы? Их имена так-то известны, можно хоть сравнением имени с константой убрать их из списка...
ImagineTables
26.06.2024 15:26Рад, что мы, наконец, перешли к разговору по существу.
А с остальными методами что делать? Бывает, например, что у кокласса диспинтерфейса есть публичные методы для вызова изнутри модуля, про которые совершено незачем знать снаружи. Например, что угодно для работы с указателями. Их тоже ручным сравнением убирать? Так тогда где же польза для реализации IDispatch.
Если мы не можем явно объявить/пометить набор для экспозинга, то это и значит: годится в рамках этой задачи рефлексия только на то, чтобы название метода не дублировать, записывая отдельно в строку:
То, что строку с именем метода можно вытащить, и всё? Или что-то большее?
Что-то большее было бы, например, если бы могли помечать нужные методы.
mayorovp
26.06.2024 15:26А с остальными методами что делать? Бывает, например, что у кокласса диспинтерфейса есть публичные методы для вызова изнутри модуля, про которые совершено незачем знать снаружи.
А с фига ли они тогда публичные? friend в помощь.
А ещё можно собирать методы не из самого класса, а из реализованных им COM интерфейсов.
Что-то большее было бы, например, если бы могли помечать нужные методы
Ну, тут надо просто ещё 3-12 лет подождать пока P1887 и P2565 "дозреют".
ImagineTables
26.06.2024 15:26А с фига ли они тогда публичные? friend в помощь.
Просто хотя бы ради интереса посмотрите на другие языки. В C#, например, ввели специальные ключевые слова для разрешения публичного доступа изнутри сборки и одновременного сокрытия от доступа снаружи. Так что, не говорите, что это никому не нужно.
Mishootk
По рефлексии энумераторов. Взял с хабра многолетней давности пример. Вся идея реализации рефлексии умещается в одном экране. Чуть доработал (убрал лямбды, у нас легаси, еще древнее стандарт), получил рефлексию перечислений. В моем проекте невозможно ждать нового стандарта.
Вся реализация в хидере. Перевод перечисления на рефлексию - замена enum EnumName {item1, item2} на ENUM(EnumName, item1, item2). Список перечислений со всем синтаксисом присвоения констант сохраняется.
Добавил сахарка, в том же списке перечислений не нарушая совместимости смог опционально добавлять еще и мультиязычные алиасы (человекочитаемый текст для вывода в контролы), а также выделение существенного имени элемента с обрезкой декорирования венгерской нотации. Конечно же обратная инициализация из строки в энумератор, итерирование по элементам.
feelamee
а ссылку не оставили (
прошу предоставить для озакомления .-.
Mishootk
Это идея для развития. Мне очень помогла.
https://habr.com/ru/articles/276763/
magrif
https://github.com/Neargye/magic_enum интересная библиотека