- PVSM.RU - https://www.pvsm.ru -

Привет! На связи Антон Полухин из Техплатформы Городских сервисов Яндекса, и сейчас я расскажу о софийской встрече Международного комитета по стандартизации языка программирования C++, в которой принимал активное участие. Это была последняя встреча, на которой новые фичи языка, с предодобренным на прошлых встречах дизайном, ещё могли попасть в C++26.
И результат превзошёл все ожидания:
std::optional<T&>
Рефлексия будет в C++26! Это просто великолепные новости, очень многие ожидали эту фичу — у многих разработчиков уже чешутся руки написать что-то интересное с её помощью.
Рефлексия в C++ отличается от рефлексии в большинстве других языков программирования, ведь она:
std::meta::info.Инструмент получился крайне мощный — он позволяет убрать множество 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 [3]. С предложением на template for (expansion statement, compile-time развёрнутый цикл) можно ознакомиться в P1306 [4].
От меня, как от пользователя языка C++, огромное спасибо всем людям, которые сделали рефлексию возможной! Это был долгий путь, который начался в 2007 году с первого предложения на добавление constepxr. С тех пор Комитет расширял возможности compile-time-вычислений: добавил constepxr-алгоритмы, разметил классы как constepxr, ввёл consteval, реализовал constepxr-аллокации и использование исключений в constepxr… — и наконец пришёл к P2996!
Приятно осознавать, что Рабочая Группа 21 [5] тоже приложила руку к этому процессу: P0031 [6], P0426 [7], P0639 [8], P0202 [9], P0858 [10], P0879 [11], P1032 [12], P2291 [13], P2417 [14]… Хотя наш вклад несравним с работой, проделанной Daveed Vandevoorde, Hana Dusíková, Faisal Vali, Andrew Sutton, Barry Revzin, Dan Katz, Peter Dimov, Wyatt Childers и многими другими людьми, годами работавшими над рефлексией и constepxr-вычислениями.
Праздник на предложении P2996 не закончился. Весьма неожиданно успели принять в стандарт P3096 [15] и P3394 [16].
Первое предложение позволяет получить типы и имена параметров функций, и в том числе — работать с конструкторами и операторами. Это мощный, но в то же время хрупкий инструмент. Например, реализации стандартных библиотек 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 [16].
Библиотека Boost.Optional долгое время позволяла создавать объекты boost::optional<T&>. В P2988 [17] эта функциональность доехала и до C++26.
Но если можно использовать просто T*, зачем же нужен std::optional<T&>? У последнего есть свои плюсы:
Радостная новость для тех, кто пользуется параллельными алгоритмами. С принятием в C++26 предложения P3179 [18] можно использовать политики выполнения (например, std::execution::par_unseq) с алгоритмами в std::ranges.
Основной автор предложения, Ruslan Arutyunyan, подсвечивает интересную фишку из данного документа: начиная с P3179 [18] 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 [18] и в P3709 [19]
Грустная новость: пока не получится использовать параллельные алгоритмы с schedulers и senders из принятого в C++26 P2300 [20]. Работа в этом направлении продолжится уже в C++29 (в P2500 [21]).
В P3111 [22] для C++26 расширили возможности атомарных переменных. Им добавили методы void store_, которые, в отличие от методов fetch_, не возвращают значение, и, соответственно, у компилятора больше возможностей для их оптимизаций.
Казалось бы, что делает эта новость в разделе про параллельные алгоритмы? А вот что: по стандарту, нельзя использовать операции atomic::fetch_ в параллельных алгоритмах с std::execution::*unseq. Операции atomic::store_ как раз позволяют обойти эту проблему — их можно использовать вместе с std::execution::*unseq.
Давайте поиграем в угадайку! Как вы думаете, почему следующий код не скомпилируется?
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 [23]:
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 [25] (документ может быть пока недоступен) предлагается добавить
std::views::input_filter, фактически убирая возможность несколько раз фильтровать один и тот же элемент, эквивалентен filter_view(to_input_view(E), P). Возможно, эту новинку удастся внести в стандарт как багфикс и увидеть решение уже в C++26.
std::string обзавёлся методом subview, который работает по аналогии с substr, но, в отличие от последнего, возвращает std::string_view (P3044 [26]).std::simd оброс новыми методами и функциональностью в P2876 [27], P3480 [28], P2664 [29], P3691 [30].std::exception_ptr теперь можно достать исключение, не выкидывая его, а используя std::exception_ptr_cast<Exception>(exception_ptr) (P2927 [31]). И можно это делать даже в compile-time (P3748 [32], может быть доступен позже).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 [34] собенно удобен при работе с 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>);std::execution::task в P3552 [35] и std::execution::write_env + std::execution::unstoppable sender-адаптеры в P3284 [36]. Теперь можно совмещать executors и корутины, C++26 теперь feature complete! И рефлексия в нём будет!
Следующий этап стандартизации С++: представители стран посылают свои замечания к C++26, подсвечивая важные баги и проблемы. Тут и вы можете внести свою лепту! Если у вас есть замечания к C++26 или любимый многострадальный баг, а может, вы знаете о какой-то проблеме — пишите нашей рабочей группе в раздел раздел «Предложения [38]»: и мы отправим ваши (исправимые на данном этапе) замечания в ISO. Разборам и исправлениям багов будут посвящены как минимум две ближайшие встречи Международного комитета.
На этом у меня всё. Приходите пообщаться на C++ Zero Cost Conf [39] 2 августа, послушать интересные и практичные доклады и пообщаться с командой userver на стенде городских сервисов Яндекса.
Пишите в комментариях о самой ожидаемой или любимой фиче в предстоящем C++26. С радостью отвечу на ваши вопросы :)
Автор: antoshkka
Источник [40]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-3/424109
Ссылки в тексте:
[1] на Гитхабе: https://github.com/userver-framework/userver/blob/develop/universal/include/userver/utils/trivial_map.hpp
[2] в видео: https://vkvideo.ru/video-77278886_456239838?t=27m35s
[3] P2996: https://wg21.link/P2996
[4] P1306: https://wg21.link/P1306
[5] Рабочая Группа 21: https://stdcpp.ru/
[6] P0031: https://wg21.link/P0031
[7] P0426: https://wg21.link/P0426
[8] P0639: https://wg21.link/P0639
[9] P0202: https://wg21.link/P0202
[10] P0858: https://wg21.link/P0858
[11] P0879: https://wg21.link/P0879
[12] P1032: https://wg21.link/P1032
[13] P2291: https://wg21.link/P2291
[14] P2417: https://wg21.link/P2417
[15] P3096: https://wg21.link/P3096
[16] P3394: https://wg21.link/P3394
[17] P2988: https://wg21.link/P2988
[18] P3179: https://wg21.link/P3179
[19] P3709: https://wg21.link/P3709
[20] P2300: https://wg21.link/P2300
[21] P2500: https://wg21.link/P2500
[22] P3111: https://wg21.link/P3111
[23] P3060: https://wg21.link/P3060
[24] ссылкой: https://www.godbolt.org/z/qrMYb3G4d
[25] P3725: https://wg21.link/P3725
[26] P3044: https://wg21.link/P3044
[27] P2876: https://wg21.link/P2876
[28] P3480: https://wg21.link/P3480
[29] P2664: https://wg21.link/P2664
[30] P3691: https://wg21.link/P3691
[31] P2927: https://wg21.link/P2927
[32] P3748: https://wg21.link/P3748
[33] P3560: https://wg21.link/P3560
[34] P2781: https://wg21.link/P2781
[35] P3552: https://wg21.link/P3552
[36] P3284: https://wg21.link/P3284
[37] P3697: https://wg21.link/P3697
[38] Предложения: https://stdcpp.ru/proposals
[39] C++ Zero Cost Conf: https://clck.ru/3MpN9W
[40] Источник: https://habr.com/ru/companies/yandex/articles/920470/?utm_source=habrahabr&utm_medium=rss&utm_campaign=920470
Нажмите здесь для печати.