Привет! На связи Антон Полухин из Техплатформы Городских сервисов Яндекса, и сейчас я расскажу о софийской встрече Международного комитета по стандартизации языка программирования C++, в которой принимал активное участие. Это была последняя встреча, на которой новые фичи языка, с предодобренным на прошлых встречах дизайном, ещё могли попасть в C++26.

И результат превзошёл все ожидания:
  • compile-time-рефлексия
  • рефлексия параметров функций
  • аннотации
  • std::optional<T&‍>
  • параллельные алгоритмы



Compile-time рефлексия


Рефлексия будет в C++26! Это просто великолепные новости, очень многие ожидали эту фичу — у многих разработчиков уже чешутся руки написать что-то интересное с её помощью.
Рефлексия в C++ отличается от рефлексии в большинстве других языков программирования, ведь она:
  • Compile-time — происходит в момент компиляции единицы трансляции.
  • Type-erased — результат рефлексирования любой сущности (типа данных, объекта, параметра, namespace) всегда представляет собой один и тот же тип: std::meta::info.
  • Императивная — работа с рефлексией идёт в привычном императивном стиле программирования (в отличие от старого метапрограммирования через специализации шаблонов).
  • Работает на уровне сущностей языка, а не на уровне токенов.
  • Опционально учитывает права доступа (public, private) текущей области видимости.
  • Обрабатывает ошибки через сompile-time-исключения.

Инструмент получился крайне мощный — он позволяет убрать множество boilerplate code при решении типовых (и не очень) задач.

Например, мы постоянно сталкиваемся с необходимостью задавать маппинг значения перечисления (enum) на его текстовое представление. В нашей кодовой базе для этого заводится специфичный bimap:

enum class Colors { kRed, kOrange, kYellow, kGreen, kBlue, kViolet };
 
constexpr userver::utils::TrivialBiMap kColorSwitch = [](auto selector) {
    return selector()
        .Case("Red", Colors::kRed)
        .Case("Orange", Colors::kOrange)
        .Case("Yellow", Colors::kYellow)
        .Case("Green", Colors::kGreen)
        .Case("Blue", Colors::kBlue)
        .Case("Violet", Colors::kViolet);
};
 
TEST(TrivialBiMap, EnumToString) {
    EXPECT_EQ(kColorSwitch.TryFind(Colors::kGreen), "Green");
    EXPECT_EQ(kColorSwitch.TryFind("Orange"), Colors::kOrange);
}

Как видите, писать такие маппинги — весьма рутинная и скучная задача. С помощью рефлексии её можно проделать единожды:

namespace impl {
    template <typename E>
    consteval auto MakeEnumLambda() {
        auto lambda = [](auto selector) {
            auto s = selector();
            template for (std::meta::info e : std::meta::enumerators_of(^^E)) {
                s.Case(
                    std::meta::extract<E>(e),
                    std::meta::identifier_of(e).remove_prefix(1)  // удаляем `k`
                );
            });
            return s;
        };
        return lambda;
    }
} // namespace impl

template <typename E>
  requires std::is_enum_v<E>
inline constexpr userver::utils::TrivialBiMap kEnum = impl::MakeEnumLambda<E>();

И после этого переиспользовать решение:

enum class Colors { kRed, kOrange, kYellow, kGreen, kBlue, kViolet };
 
TEST(TrivialBiMap, EnumToString) {
    EXPECT_EQ(kEnum<Colors>.TryFind(Colors::kGreen), "Green");
    EXPECT_EQ(kEnum<Colors>.TryFind("Orange"), Colors::kOrange);
}

Что за utils::TrivialBiMap?
Это контейнер для хранения известных на compile-time-данных. Контейнер позволяет молниеносно искать по ключу и по значению за O(1). При этом искать намного быстрее, чем unordered-контейнеры и flat_map. Мы активно им пользуемся во фреймворке ? userver в Техплатформе Городских сервисов Яндекса. Исходники его можно посмотреть на Гитхабе, а описание принципа его работы есть в видео.

Предложение по рефлексии и больше примеров можно увидеть в P2996. С предложением на template for (expansion statement, compile-time развёрнутый цикл) можно ознакомиться в P1306.

От меня, как от пользователя языка C++, огромное спасибо всем людям, которые сделали рефлексию возможной! Это был долгий путь, который начался в 2007 году с первого предложения на добавление constepxr. С тех пор Комитет расширял возможности compile-time-вычислений: добавил constepxr-алгоритмы, разметил классы как constepxr, ввёл consteval, реализовал constepxr-аллокации и использование исключений в constepxr… — и наконец пришёл к P2996!

Приятно осознавать, что Рабочая Группа 21 тоже приложила руку к этому процессу: P0031, P0426, P0639, P0202, P0858, P0879, P1032, P2291, P2417… Хотя наш вклад несравним с работой, проделанной Daveed Vandevoorde, Hana Dusíková, Faisal Vali, Andrew Sutton, Barry Revzin, Dan Katz, Peter Dimov, Wyatt Childers и многими другими людьми, годами работавшими над рефлексией и constepxr-вычислениями.

Рефлексия аннотаций и параметров функций


Праздник на предложении P2996 не закончился. Весьма неожиданно успели принять в стандарт P3096 и P3394.

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

Второе предложение позволяет делать собственные аннотации (не путать с атрибутами!), которые доступны для рефлексии и дают возможность дополнительно настраивать кодогенерацию.

У аннотаций есть синтаксис [[=constant-expression]], где constant-expression может быть любым выражением, вычислимым на этапе компиляции.

Например, без аннотаций можно сделать вот такую функцию, которая будет выводить имена полей структуры вместе со значениями полей:

namespace my_reflection {

template <typename T>
void PrintKeyValue(const T& value) {
    template for (constexpr auto field : nonstatic_data_members_of(^^T)) {
        std::println("{}: {}", identifier_of(field), value.[: field :]);
    }
}

}  // namespace my_reflection

// Пример использования:
struct Pair {
    int first;
    int y;
};
my_reflection::PrintKeyValue(Pair{1, 2});
// Вывeдет в консоль:
// first: 1
// y: 2

А с помощью аннотаций можно переопределять имена полей:

namespace my_reflection {

struct Name{ std::string_view name; };

template <typename T>
void PrintKeyValue(const T& value) {
    template for (constexpr auto field : nonstatic_data_members_of(^^T)) {
        constexpr auto annotation_vec = annotations_of(field);
        constexpr std::string_view name = (
            annotation_vec.size() == 1
                && type_of(annotation_vec[0]) == ^^my_reflection::Name
            ? std::meta::extract<my_reflection::Name>(annotation_vec[0]).name
            : identifier_of(field)
        );
        std::println("{}: {}", name, value.[: field :]);
    }
}

}  // namespace my_reflection

// Пример использования:
struct Pair {
    int first;
    [[=my_reflection::Name{"second"}]]
    int y;
};
my_reflection::PrintKeyValue(Pair{1, 2});
// Вывeдет в консоль:
// first: 1
// second: 2

Ещё больше примеров доступно в самом предложении P3394.

std::optional<T&‍>


Библиотека Boost.Optional долгое время позволяла создавать объекты boost::optional<T&‍>. В P2988 эта функциональность доехала и до C++26.

Но если можно использовать просто T*, зачем же нужен std::optional<T&‍>? У последнего есть свои плюсы:
  • запрещает арифметику указателей, избавляя от части возможных ошибок;
  • не вызывает недоумения (а надо ли освобождать ресурсы по этому указателю?);
  • имеет удобные для использования монадические интерфейсы и удобные value_or()-функции;
  • может передаваться как диапазон в ranges.


Параллельные алгоритмы


Радостная новость для тех, кто пользуется параллельными алгоритмами. С принятием в C++26 предложения P3179 можно использовать политики выполнения (например, std::execution::par_unseq) с алгоритмами в std::ranges.

Основной автор предложения, Ruslan Arutyunyan, подсвечивает интересную фишку из данного документа: начиная с P3179 ranges начинают использоваться как выходной параметр. Вместо std::ranges::copy(std::execution::par, in, out.begin()); мы получаем более безопасный и короткий интерфейс вида std::ranges::copy(std::execution::par, in, out);.

Если выходной диапазон меньше, чем входной, не произойдёт проезда по памяти — скопируется лишь то количество элементов, которое можно скопировать в выходной диапазон. Более того, если пользователь передал выходной диапазон меньшего размера по ошибке, у него всегда есть возможность это определить: все алгоритмы возвращают точку, до которой они смогли дойти во входном диапазоне (входных диапазонах). Особенными в этом отношении являются ranges::reverse_copy и ranges::rotate_copy. Кому интересно, могут почитать о последних двух алгоритмах в P3179 и в P3709

Грустная новость: пока не получится использовать параллельные алгоритмы с schedulers и senders из принятого в C++26 P2300. Работа в этом направлении продолжится уже в C++29 (в P2500).

В P3111 для C++26 расширили возможности атомарных переменных. Им добавили методы void store_, которые, в отличие от методов fetch_, не возвращают значение, и, соответственно, у компилятора больше возможностей для их оптимизаций.

Казалось бы, что делает эта новость в разделе про параллельные алгоритмы? А вот что: по стандарту, нельзя использовать операции atomic::fetch_ в параллельных алгоритмах с std::execution::*unseq. Операции atomic::store_ как раз позволяют обойти эту проблему — их можно использовать вместе с std::execution::*unseq.

Ещё немного о ranges


Давайте поиграем в угадайку! Как вы думаете, почему следующий код не скомпилируется?

for (auto x : std::ranges::iota(0, some_vector.size())) {
    std::cout << some_vector[x] << std::endl;
}

Разгадка
Код не скомпилируется со словами no matching function for call to 'iota_view(int, long unsigned int)', так как iota требует одинаковые типы входных параметров.

Как раз чтобы не сталкиваться с такой проблемой и не писать лишнего, в C++26 был добавлен std::ranges::indices в P3060:

for (auto x : std::ranges::indices(some_vector.size())) {
    std::cout << some_vector[x] << std::endl;
}

Продолжим с нашей угадайкой. Теперь загадка от Nicolai Josuttis. Что произойдёт в следующем примере?

std::vector<std::string> coll1{"Amsterdam", "Berlin", "Cologne", "LA"};
  // Перемещаем длинные строки в обратном порядке в другой контейнер
  auto large = [](const auto& s) { return s.size() > 5; };
  auto sub = coll1 | std::views::filter(large)
                   | std::views::reverse
                   | std::views::as_rvalue
                   | std::ranges::to<std::vector>();

Разгадка
А вот тут будет проезд по памяти и Segmentation Fault. Почему? Воспользуйтесь ссылкой и попробуйте раздебажить. Добавление std::println в фильтр может помочь.

Проблема кроется прямо в дизайне std::views::filter. Увы, фильтр позволяет проходить по диапазону несколько раз, при этом он не накладывает константность на данные. Как результат — данные можно «вытащить» или изменить, и при последующих прохождениях фильтр будет сходить с ума. Nicolai Josuttis приводит ещё пример, который является неопределённым поведением (undefined behavior, UB) с точки зрения стандарта:

// Возвращаем умерших монстров к жизни
auto dead = [] (const auto& m) { return m.isDead(); };
for (auto& m : monsters | std::views::filter(dead)) {
  m.bringBackToLive();  // undefined behavior
}

Если бы после фильтра был ещё, например, std::views::reverse, код мог бы сломаться.

Чтобы обойти все эти ужасы c std::views::filter, в P3725 (документ может быть пока недоступен) предлагается добавить
std::views::input_filter, фактически убирая возможность несколько раз фильтровать один и тот же элемент, эквивалентен filter_view(to_input_view(E), P). Возможно, эту новинку удастся внести в стандарт как багфикс и увидеть решение уже в C++26.

Прочие новинки


  • std::string обзавёлся методом subview, который работает по аналогии с substr, но, в отличие от последнего, возвращает std::string_view (P3044).
  • std::simd оброс новыми методами и функциональностью в P2876, P3480, P2664, P3691.
  • Из std::exception_ptr теперь можно достать исключение, не выкидывая его, а используя std::exception_ptr_cast<Exception>(exception_ptr) (P2927). И можно это делать даже в compile-time (P3748, может быть доступен позже).
  • В последний момент проскочило предложениеP3560, которое меняет способ сообщения об ошибке для рефлексии. То, что раньше было ошибкой компиляции, теперь стало исключением, выкинутым на этапе компиляции, — его можно ловить и обрабатывать.
  • Из приятных мелочей — в C++26 добавили класс std::constant_wrapper и переменную constexpr std::cw. Это более краткая замена для std::integral_constant. При этом они обладают всеми операторами нижележащего типа, что позволяет использовать их как обычные числа, но передавать в функцию как compile-time-константы:

  void sum_is_42(auto x, auto y) {
    static_assert(x + y == 42);
  }
  sum_is_42(std::cw<40>, std::cw<2>);

  • std::cw из предложения P2781 собенно удобен при работе с std::mdspan. Например, std::mdspan(data, std::integral_constant<std::size_t, 10>{}, std::integral_constant<std::size_t, 20>{}, std::integral_constant<std::size_t, 30>{});, превращается просто в std::mdspan(data, std::cw<10>, std::cw<20>, std::cw<30>);
  • В executors добавили std::execution::task в P3552 и std::execution::write_env + std::execution::unstoppable sender-адаптеры в P3284. Теперь можно совмещать executors и корутины, чтобы ещё сильнее смущать коллег на код-ревью.
  • Наконец в P3697 продолжили завинчивание гаек с безопасностью, и ещё больше функций стандартной библиотеки обросли hardening-проверками.


Итоги


C++26 теперь feature complete! И рефлексия в нём будет!

Следующий этап стандартизации С++: представители стран посылают свои замечания к C++26, подсвечивая важные баги и проблемы. Тут и вы можете внести свою лепту! Если у вас есть замечания к C++26 или любимый многострадальный баг, а может, вы знаете о какой-то проблеме — пишите нашей рабочей группе в раздел раздел «Предложения»: и мы отправим ваши (исправимые на данном этапе) замечания в ISO. Разборам и исправлениям багов будут посвящены как минимум две ближайшие встречи Международного комитета.

На этом у меня всё. Приходите пообщаться на C++ Zero Cost Conf 2 августа, послушать интересные и практичные доклады и пообщаться с командой userver на стенде городских сервисов Яндекса.

Пишите в комментариях о самой ожидаемой или любимой фиче в предстоящем C++26. С радостью отвечу на ваши вопросы :)

Комментарии (35)


  1. AlexeyK77
    01.07.2025 07:08

    очень давно не программирую. Но посмотрев листинги с этими всеми жуткими шаблонами на меташаблонах, понимаю, что С++ превратился во что-то другое, не приспособленное для человечьих мозгов.

    Верните обратно С++93 ;)

    Хотя лично я болею за Rust и надеюсь, что при смене поколений этот "монстрик" уйдет туда-же куда ушел кобол с программистами мэйнфреймов.


    1. denis_iii
      01.07.2025 07:08

      Да, основная проблема С++ в том, что с каждым новым релизом он становится все более синтаксически нечитаемым. Но, главное, что бы AI-агенты справлялись.
      А для души и быстрой компиляции всегда есть 14 и 17.


    1. Jijiki
      01.07.2025 07:08

      шаблоны становятся доступнее, маленькие тесты делал, вроде нормально, вот по старинке писать это тогда только С как раз(тоесть всё расписывать и тд)

      с++ удобно есть operator - это удобно

      добавили std::println("{}",1); вид вывода - тоже удобно

      простенькие лямбды без std тоже удобно

      сама стд стала всё равно удобнее

      на расте же тоже шаблонный синтаксис как я понимаю <> с такими кавычками


      1. domix32
        01.07.2025 07:08

        Там есть несколько механизмов, но да с угловыми скобками. Местные концепты там тоже можно писать через where clause, а не фигачить все в параметры шаблонов. В новейших стандартах вроде как завезли require clauses, но там как обычно.


    1. dv0ich
      01.07.2025 07:08

      У Раста-то синтаксис охренеть какой читаемый, лол. Даже в сравнении с плюсами.


  1. Nuflyn
    01.07.2025 07:08

    Где-то мы видели Option<T>. Ах да в Расте)


    1. Kelbon
      01.07.2025 07:08

      Какая же глубокая экспертиза, нигде никогда не было optional (или T? в каких-то языках) и в стандартной библиотеке С++ не было optional уже около 10 лет


      1. wander
        01.07.2025 07:08

        Который пришел туда из boost, где был c 2003 года.


  1. Kelbon
    01.07.2025 07:08

    template for конечно ошибка, но при условии что компиляторщики смогут это реализовать без слома всего, рефлексия в целом перевесит этот недостаток

    У меня такой вопрос: можно ли пройти по всем методам типа, посмотреть их типы (т.е. например int(float, double) ) ? И что насчёт шаблонных методов?


    1. antoshkka Автор
      01.07.2025 07:08

      С шаблонными методами и параметрами работа поддержана, получить список функций можно через members_of. Так что кажется что можно все :)


      1. Kelbon
        01.07.2025 07:08

        как template for взаимодействует с break; / continue; ? Если никак, может выкинуть его?


        1. antoshkka Автор
          01.07.2025 07:08

          Всё работает как ожидается. break перепрыгивает за конец цикла, continue перепрыгивает к началу на проверку условий


          1. Kelbon
            01.07.2025 07:08

            тут же есть разница между break на constexpr условии и на рантайм условии, неочевидно как это будет работать


        1. KanuTaH
          01.07.2025 07:08

          Да вроде нормально взаимодействует. Как и с return внутри себя.


  1. Jijiki
    01.07.2025 07:08

    // Возвращаем умерших монстров к жизни
    auto dead = [] (const auto& m) { return m.isDead(); };
    for (auto& a : monsters | std::views::filter(dead)) {
      a.bringBackToLive();  // а так?
    }


    1. Kelbon
      01.07.2025 07:08

      а что изменилось

      P.S. если вы про имя параметра, то в С++ несколько другие правила видимости нежели в JS


      1. Jijiki
        01.07.2025 07:08

        Скрытый текст

        понял, тут правда переставил сборку по фильтру как я понял-это сборка по фильтру и перенес её действие до цикла

        а вообще если там уб то логично же, надо собрать же, сделать проверку с последовательностью по фильтру, а в фильтре лямбда, это даёт последовательность и уже пройтись по последовательности так звучит ожидаемо и очевидно


        1. KanuTaH
          01.07.2025 07:08

          и перенес её действие до цикла

          Вообще-то нет.

          это даёт последовательность и уже пройтись по последовательности так звучит ожидаемо и очевидно

          У вас не делается ничего подобного. Вставьте отладочную печать скажем в лямбду и непосредственно перед строкой с for и убедитесь.


          1. Jijiki
            01.07.2025 07:08

            вы правы,

            Скрытый текст
            #include <cstdint>
            #include <cstring>
            #include <iostream>
            #include <vector>
            #include <ranges>
            #include <string>
            class test{
                public:
                std::string name;
                int p=1;
                    int te(){return p;}
                    void pName(){std::cout<<name<<std::endl;}
                    void bringBackToLive(){std::cout<<p<<std::endl;}
            };
            
            int main (){
                std::vector<test> r(10);
                std::string tttt[]={"Name0","Name1","Name2","Name3","Name4","Name5","Name6","Name7","Name8","Name9"};
                int o=0;
                int c=0;
                for(auto& k: r){
            
                    if(o%2){k.p=o;k.name=tttt[c];}o++;c++;
            
                }
            
                //std::vector<test> b(10);
                
                
                auto b= r|std::views::filter([](const auto& m) {std::cout<<"inlambda"<<std::endl; return m.p>1; });
                
                std::cout<<"NOTLAMBDA"<<std::endl;
                for (auto& v :b) {
                    v.pName();
                    v.bringBackToLive();
                }
                return 0;
            }
            
            NOTLAMBDA
            inlambda
            inlambda
            inlambda
            inlambda
            Name3
            3
            inlambda
            inlambda
            Name5
            5
            inlambda
            inlambda
            Name7
            7
            inlambda
            inlambda
            Name9
            9
            
            
            тогда можно еще просто так 
                auto b= [](auto m) {
                    for(auto x: m){
                        if(x.p>1){
                        x.pName();
                        x.bringBackToLive();
                        }
                    } 
                };
            
                b(r);

            забыл что это лямбда и расслабился

            ааааааа, это как функция на момент вызова в цикле for чтоли?

            поидее обратились к б в цикле пошла лямбда поидее так же, сначала я растерялся, но поидее всё равно логично, логично даже если лямбду записать вместо б, тогда будет нагляднее просто


  1. JordanCpp
    01.07.2025 07:08

    Почему с рефлексией так тянули, аж с 2007? Малый интерес или неготовность ядра С++ на то время?


    1. antoshkka Автор
      01.07.2025 07:08

      Интерес огромный, а вот с возможностями compile time вычислений в 2007 было очень тяжко. В C++11 constexpr функция должна была состоять только из одного return, в C++14 уже можно было делать циклы... А потом уже появились динамические аллокации в constepr, исключения, стандартная библиотека подтянулась


      1. JordanCpp
        01.07.2025 07:08

        Как говорится дождались, осталось дождаться когда реализуют в компиляторах.


        1. antoshkka Автор
          01.07.2025 07:08

          Прототипы рефлексии есть для clang, и кажется что для GCC тоже. Так что надеюсь, что реализуют сравнительно быстро, тем более что изменения затрагивают только frontend компилятора (в отличие от модулей, которые затрагивают вообще всё, включая системы сборки)


  1. Lainhard
    01.07.2025 07:08

    Кастомные [[my_attr]] завезли?

    Упс. Недочитал статью и сразу полез в комментарии. Не смейте меня прощать. Вопрос снимается.


  1. JordanCpp
    01.07.2025 07:08

    Теперь фраза "C++ уже не тот" с новыми фичами, заиграла другими красками.


  1. NeoCode
    01.07.2025 07:08

    Не знаю что и сказать. Рад за С++, что он развивается, рефлексия это реально важная вещь для языков программирования; но примеров кода не понял вообще (последнее время пишу в основном или на чистом Си или на Qt5)

    А рефлексию (для енумов и не только) я делаю вот так:

    #define LIST \
      ITEM(ONE, "hello", x) \
      ITEM(TWO, "world", y) \
      ITEM(THREE, "!",   z)
    
    // перечисление
    #define ITEM(a, b, c) a,
    enum Foo {
      LIST
    };
    
    // имена элементов перечисления
    #define ITEM(a, b, c) #a,
    const char *names[] = {
      LIST
    };
    
    // ассоциированные строки
    #define ITEM(a, b, c) b,
    const char *strings[] = {
      LIST
    };
    
    // и т.д. - декларация структуры, имена полей структуры, аргументы функции, что угодно

    Самое прикольное что эта фича работала еще до появления самого С++, более того она, вероятно, была еще когда меня на свете не было:) Но в книгах этого приема обычно нет, и люди мучаются, выдумывая всякие костыли или просто тупо вручную пишут код и рискуют однажды забыть дописать очередной элемент.


    1. tenzink
      01.07.2025 07:08

      Это же стандартный https://en.wikipedia.org/wiki/X_macro. Не сильно раскрученная техника, но, мне казалось, довольно известная


    1. X-Ray_3D
      01.07.2025 07:08

      
      namespace Impl {
      
      namespace ranges = std::ranges;
      using std::array;
      using std::string_view;
      
      template <class Ty>
      inline constexpr bool hasStrings = false;
      
      template <class Ty>
      inline constexpr Ty Tokens = Ty{};
      
      template <auto EnumVal>
      static consteval auto enumName() {
      #ifdef _MSC_VER
          // MSVC: auto __cdecl Impl::enumName<align::CM>(void)
          constexpr string_view sv{__FUNCSIG__};
          constexpr size_t last = sv.find_last_of(">");
      #else
          // clang: auto Impl::name() [E = align::CM]
          // gcc: consteval auto Impl::name() [with auto E = align::CM]
          constexpr string_view sv{__PRETTY_FUNCTION__};
          constexpr auto last = sv.find_last_of("]");
      #endif
          constexpr size_t first = sv.find_last_of(":", last) + 1;
          array<char, last - first + 1> buf{}; // +1 '\0' tetminated c_str
          ranges::copy(string_view{sv.data() + first, last - first}, buf.begin());
          return buf;
      }
      
      template <auto EnumVal>
      inline constexpr auto ENameArr{enumName<EnumVal>()};
      template <auto EnumVal>
      inline constexpr string_view EnumName{ENameArr<EnumVal>.data(), ENameArr<EnumVal>.size() - 1};
      
      template <typename Enum, auto... Enums>
      class Tokenizer {
          struct Data {
              string_view name;
              Enum value;
          };
          static constexpr array tokens{
              Data{EnumName<Enums>, Enums}
              ...
          };
          using Ty = std::underlying_type_t<Enum>;
          static constexpr Enum errorValue{static_cast<Enum>(std::numeric_limits<Ty>::max())};
      
      public:
          static constexpr auto toString(Enum e) noexcept {
              auto it = ranges::find(tokens, e, &Data::value);
              return it == tokens.end() ? string_view{""} : it->name;
          }
          static constexpr Enum toEnum(string_view str) noexcept {
              auto it = ranges::find(tokens, str, &Data::name);
              return it == tokens.end() ? errorValue : it->value;
          }
      };
      
      } // namespace Impl
      
      #define XML_ENUM(Enum, ...)                                                            \
          enum class Enum : int {                                                            \
              __VA_ARGS__                                                                    \
          };                                                                                 \
          inline auto operator+(Enum e) noexcept { return std::underlying_type_t<Enum>(e); } \
          namespace Impl {                                                                   \
          template <>                                                                        \
          inline constexpr auto hasStrings<Enum> = true;                                     \
          template <>                                                                        \
          inline constexpr auto Tokens<Enum> = [] {                                          \
              using enum Enum; /* using enum ↓  P1099R5 */                                 \
              return Tokenizer<Enum, __VA_ARGS__>{};                                         \
          }();                                                                               \
          }
      
      template <typename E, typename Enum = std::remove_cvref_t<E>>
          requires Impl::hasStrings<Enum>
      inline constexpr auto enumToString(E e) {
          return Impl::Tokens<Enum>.toString(e);
      }
      
      template <typename E, typename Enum = std::remove_cvref_t<E>>
          requires Impl::hasStrings<Enum>
      inline constexpr E stringToEnum(std::string_view str) {
          return Impl::Tokens<Enum>.toEnum(str);
      }
      
      // Параметр надписей (ярлыков): способ выравнивания текста. Значение по умолчанию – CM.
      XML_ENUM(align,
          CM, // по центру
          LT, // по левому верхнему углу
          CT, // по верхнему краю
          RT, // по правому верхнему углу
          LM, // по левому краю
          RM, // по правому краю
          LB, // по левому нижнему углу
          CB, // по нижнему краю
          RB  // по правому нижнему углу
      )
        

      Наконец-то без макродристни это по-человечески просто писать можно будет.

      enum class align{
          CM, // по центру
          LT, // по левому верхнему углу
          CT, // по верхнему краю
          RT, // по правому верхнему углу
          LM, // по левому краю
          RM, // по правому краю
          LB, // по левому нижнему углу
          CB, // по нижнему краю
          RB  // по правому нижнему углу
      };


  1. jaobabus
    01.07.2025 07:08

    Почему именно template for, а не for constexpr по аналогии с if constexpr?


    1. KanuTaH
      01.07.2025 07:08

      Ну, насколько я понимаю, разница в именовании проистекает из принципа работы. template for - это именно шаблон, по которому компилятор перегенерирует "тело цикла" для каждого элемента из указанного списка. А if constexpr сам по себе никакой генерацией кода не занимается, компилятор просто выбирает один из вариантов в соответствии со значением constexpr условия. По-моему достаточно логично.


  1. domix32
    01.07.2025 07:08

    Увы, фильтр позволяет проходить по диапазону несколько раз, при этом он не накладывает константность на данные.

    чёт я не понял, а сфига ли он проходит несколько раз? Казалось бы, рэнж должен был быть в размер контейнера/ Где что-то пошло не так и он начинает бродить по "кругу"?

    причем если делать reverse|filter ему нормально, и ожидаемо перемещённые значения рендерятся как пустые, а filter|reverse это почему-то ересь.


  1. Jijiki
    01.07.2025 07:08

    кстати std::views::filter используется неоправдно в цикле кстати, тоесть проблема в том что это погоня за другим языком, а в тех примерах совсем другие подходы и работа с памятью, если это тащить в С++ будут проблемы ) мне кажется) тоесть зачем нам проходить 2 раза по данным? надо идти 1 раз по данным производя очевидные проверки, а так понаставишь таких фильтров я чувствую и что-то не то, тоесть сначало собрали, и что удалили когда вошли в цикл?

    чото я с фильтром запутался не делает ли он копий в памяти я этот вопрос еще давно задавал в другом языке частенько


    1. mamaximov
      01.07.2025 07:08

      Да вообще непонятно зачем это сделано. Иногда есть ощущение что создали стандарта сознательно закладывают мины в язык, которые потом чинят усложняя язык и стандартную билиотеку. Лично я никак это иначе не могу объяснить. Причем если рассматривать range-ы. Они же не первопроходцы. Уже были аналоги в других языках. Нигде никто ничего не кеширует в filter-е (если брать mainstream). Более того в например в Java-е, повторная итерация по stream-у это вообще прохая практика, сразу получите exception. Да и просто если посмотреть на api range-ов (взгляд из другой экосистемы) он кажется неудачный. Да очень элегантно сделали, но есть фатальный недостаток не работает "." (не будет нормальной поддержки со стороны IDE), нужно помнить какие есть views-ы или постоянно набирать std::ranges::views или alias какой-то использовать. Может не очень актуально во времена llm, но тем не менее


      1. antoshkka Автор
        01.07.2025 07:08

        Это обычная ситуация для всех языков программирования, которые активно развиваются. Вон даже в простом C сравнительно недавно напортачили с extern inline.

        Просто зачастую у нас есть язык, за которым внимательно следим, и не очень замечаем что в других проблемы не меньше.


  1. olku
    01.07.2025 07:08

    Это София которая в Болгарии? Нажал ссылку - "clck.ru refused to connect"