Привет! На связи Антон Полухин из Техплатформы Городских сервисов Яндекса, и сейчас я расскажу о софийской встрече Международного комитета по стандартизации языка программирования 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. С радостью отвечу на ваши вопросы :)

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


  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. Nuflyn
    01.07.2025 07:08

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


    1. Kelbon
      01.07.2025 07:08

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


  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. 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. 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. jaobabus
    01.07.2025 07:08

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