
Привет! На связи Антон Полухин из Техплатформы Городских сервисов Яндекса, и сейчас я расскажу о софийской встрече Международного комитета по стандартизации языка программирования 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);
}
Предложение по рефлексии и больше примеров можно увидеть в 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>();
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
и переменную constexprstd::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)
Kelbon
01.07.2025 07:08template for конечно ошибка, но при условии что компиляторщики смогут это реализовать без слома всего, рефлексия в целом перевесит этот недостаток
У меня такой вопрос: можно ли пройти по всем методам типа, посмотреть их типы (т.е. например int(float, double) ) ? И что насчёт шаблонных методов?
Jijiki
01.07.2025 07:08// Возвращаем умерших монстров к жизни auto dead = [] (const auto& m) { return m.isDead(); }; for (auto& a : monsters | std::views::filter(dead)) { a.bringBackToLive(); // а так? }
Kelbon
01.07.2025 07:08а что изменилось
P.S. если вы про имя параметра, то в С++ несколько другие правила видимости нежели в JS
Jijiki
01.07.2025 07:08Скрытый текст
понял, тут правда переставил сборку по фильтру как я понял-это сборка по фильтру и перенес её действие до цикла
а вообще если там уб то логично же, надо собрать же, сделать проверку с последовательностью по фильтру, а в фильтре лямбда, это даёт последовательность и уже пройтись по последовательности так звучит ожидаемо и очевидно
KanuTaH
01.07.2025 07:08и перенес её действие до цикла
Вообще-то нет.
это даёт последовательность и уже пройтись по последовательности так звучит ожидаемо и очевидно
У вас не делается ничего подобного. Вставьте отладочную печать скажем в лямбду и непосредственно перед строкой с
for
и убедитесь.
JordanCpp
01.07.2025 07:08Почему с рефлексией так тянули, аж с 2007? Малый интерес или неготовность ядра С++ на то время?
antoshkka Автор
01.07.2025 07:08Интерес огромный, а вот с возможностями compile time вычислений в 2007 было очень тяжко. В C++11 constexpr функция должна была состоять только из одного return, в C++14 уже можно было делать циклы... А потом уже появились динамические аллокации в constepr, исключения, стандартная библиотека подтянулась
JordanCpp
01.07.2025 07:08Как говорится дождались, осталось дождаться когда реализуют в компиляторах.
Lainhard
01.07.2025 07:08Кастомные [[my_attr]] завезли?
Упс. Недочитал статью и сразу полез в комментарии. Не смейте меня прощать. Вопрос снимается.
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 }; // и т.д. - декларация структуры, имена полей структуры, аргументы функции, что угодно
Самое прикольное что эта фича работала еще до появления самого С++, более того она, вероятно, была еще когда меня на свете не было:) Но в книгах этого приема обычно нет, и люди мучаются, выдумывая всякие костыли или просто тупо вручную пишут код и рискуют однажды забыть дописать очередной элемент.
tenzink
01.07.2025 07:08Это же стандартный https://en.wikipedia.org/wiki/X_macro. Не сильно раскрученная техника, но, мне казалось, довольно известная
AlexeyK77
очень давно не программирую. Но посмотрев листинги с этими всеми жуткими шаблонами на меташаблонах, понимаю, что С++ превратился во что-то другое, не приспособленное для человечьих мозгов.
Верните обратно С++93 ;)
Хотя лично я болею за Rust и надеюсь, что при смене поколений этот "монстрик" уйдет туда-же куда ушел кобол с программистами мэйнфреймов.
denis_iii
Да, основная проблема С++ в том, что с каждым новым релизом он становится все более синтаксически нечитаемым. Но, главное, что бы AI-агенты справлялись.
А для души и быстрой компиляции всегда есть 14 и 17.
Jijiki
шаблоны становятся доступнее, маленькие тесты делал, вроде нормально, вот по старинке писать это тогда только С как раз(тоесть всё расписывать и тд)
с++ удобно есть operator - это удобно
добавили std::println("{}",1); вид вывода - тоже удобно
простенькие лямбды без std тоже удобно
сама стд стала всё равно удобнее
на расте же тоже шаблонный синтаксис как я понимаю <> с такими кавычками