cpp

Привет! На связи Антон Полухин из Техплатформы Городских сервисов Яндекса. На днях в Кройдоне состоялась встреча международного комитета по стандартизации языка программирования C++, в которой я принимал активное участие. В этот раз (как и в прошлый), всё внимание было сосредоточено на C++26 и… теперь он готов! Осталось пройти формальные этапы в вышестоящих инстанциях ISO, и мы получим C++26 который заслужили. В нём будут:

  • reflection,

  • контракты,

  • SIMD,

  • линейная алгебра,

  • расширенные возможности сonstexpr,

  • hardening,

  • Hazard Pointer и RCU,

  • #embed,

  • executors,

  • и многие другие полезные вещи.

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

std::is_within_lifetime

На одной из встреч в C++ была добавлена функция для проверки, что объект p валидный и его можно использовать в compile‑time‑выражении:

template<class T>
consteval bool is_within_lifetime(const T* p) noexcept;

Например, с помощью этой функции можно работать c union в compile‑time, спрашивая напрямую у компилятора, какой из элементов union сейчас активен. Пример реализованного на этом подходе std::optional есть в P2641R4.

Но автор решил не останавливаться на достигнутом! С P3450 функция научилась извлекать из компилятора информацию, можно ли делать downcast:

А почему столько внимания к is_within_lifetime?

Действительно, приблизительно то же самое можно получить с использованием dynamic_cast, если он не отключен через флаги компилятора.

Однако, std::is_within_lifetime() интересен тем, что открывает дверку к тому, чтобы C++ начал обзаводиться consteval функциями “вытаскивающими” тайные знания из компилятора. Например, позволяет “посмотреть” активный элемент union, что иначе невозможно реализовать без добавления дополнительных переменных (см. имплементацию std::variant).

Что если спрашивать у компилятора, и другие скрытые свойства? Сделана ли value initialization для переменной (занулена ли она?). Или запрашивать максимально используемый размер стека у функции? Наверняка можно придумать и другие подобные функции, пишите в комментариях, если у вас есть идеи…

std::atomic_ref::address()

Как я уже говорил в прошлых статьях о C++26, комитет настроился на большую безопасность и надёжность языка. Одна из таких новинок — изменение возвращаемого типа данных из T* std::atomic_ref<T>::address() на void* (P3936).

С T* проблема в следующем: люди совершают ошибки наличие множества разадресовываемых указателей в функции приводит к ошибкам. Особенно неприятно, когда такой код связан с многопоточностью — тогда ошибку крайне сложно найти. Например, если есть желание сравнить адреса переменных или посчитать хеши от адреса, то легко ошибиться и написать лишний * (вместо std::hash{}(ref.address()) написать std::hash{}(*ref.address()).

Ranges

std::views::filter обладает весьма неожиданными подводными камнями. Например:

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

А вот этот пример вообще не скомпилируется:

void constIterate(const auto& coll) {
      for (const auto& elem: coll) {
          //...
      }
}

// ...

std::vector<std::string> coll{"Amsterdam", "Berlin", "Cologne", "LA"};
auto large = [](const auto& s) { return s.size() > 5; };
constIterate(coll | std::views::filter(large));  // compile‐time ERROR

И наконец, мучительная и сложно осознаваемая проблема:

  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>();

Это только некоторые из проблем, на которые обратила внимание РГ от России и отправила комментарии в международный комитет C++ (и не мы одни!). И на встрече в Кройдоне проблема обрела частичное решение в P3725! Теперь общая рекомендация от комитета такова: по умолчанию перед фильтром всегда использовать std::views::as_input |. Такая конструкция уберёт лишнее кэширование из фильтра, заставит его быть гарантированно однопроходным и в целом работать без сюрпризов:

// Воскрешаем монстров, правильная и более быстрая версия:
auto dead = [] (const auto& m) { return m.isDead(); };
for (auto& m : monsters | std::views::as_input | std::views::filter(dead)) {
 m.bringBackToLive();  // OK
}

Изначальный план по решению проблемы включал в себя создание безопасного аналога std::views::filter. К несчастью, сейчас уже достаточно поздно вносить новый std::views в C++26, поэтому std::views::safe_filter появится только в C++29. Однако его легко реализовать самостоятельно:

namespace impl {
   struct safe_view_impl {
       template <class Filter>
       static constexpr auto operator()(Filter value) {
           return std::views::as_input | std::views::filter(std::move(value));
       }
   };
}  // namespace impl;

inline constexpr impl::safe_view_impl safe_view{};

С правками из P3725 в нём заработает и constIterate. Есть и полный пример для экспериментов.

Ах да! Если вы гадали, что такое std::views::as_input, то это бывший std::views::to_input, который решили переименовать, чтобы было консистентно с std::views::as_rvalue в P3828.

Прочие замечания от разработчиков из России

В P4037 поправили переносимость кода для заголовочного файла <random>. Теперь unsigned char и signed char обязаны работать с uniform_int_distribution и другими классами из этого заголовочного файла. 

Код uniform_int_distribution<std::uint8_t> стал переносимым на всех стандартах C++ (замечание приняли как исправление бага). Использование прочих неподдерживаемых типов перестало быть UB и теперь диагностируется во время компиляции.

На озвученные выше проблемы с <random> мы напоролись пару лет назад в Техплатформе Городских сервисов Яндекса — и нам не понравилось.

Другое наше замечание: разрешить обходиться без дополнительной косвенности при использовании std::function_ref. Например, во фреймворке 🐙 userver мы активно используем function_ref, и нам бы не хотелось иметь дополнительную индирекцию при вызове function_ref, созданного от move_only_function или copyable_function. Оптимизацию разрешили в P3961.

И ещё несколько улучшений

  • std::simd получил множество небольших улучшений в P3690, P3844, P3932, P4012.

  • std::runtime_format переименовали в std::dynamic_format, чтобы не было путаницы, ведь этот класс теперь можно использовать в compile‑time, а не только в runtime (P3953).

  • Арифметические операции с насыщением были тоже переименованы, чтобы избежать непонимания, что же значит sat_. В P4052 префикс перестал быть сокращённым и превратился в saturation_.

  • Множество улучшений приземлилось в executors. 

    • В P4052 были заменены _t на tag для sender_t, scheduler_t, operation_state_t, receiver_t, чтобы не путать теги с алиасами. 

    • В P3980 поженили std::task с аллокаторами executors. 

    • Документ P3826 переосмыслил механизм кастомизации sender алгоритмов. 

    • parallel_scheduler улучшился в P3804: он избавился от виртуального деструктора и научился работать не только с inplace_stop_token (и это ещё не все улучшения!). 

    • А ещё улучшения пришли в P3986, P3373, P3941, P4159, P4159.

  • В P4159 std::span лишился конструктора от std::initializer_list. Зато в P3787R добавили возможность для std(::ranges)::uninitialized_fill* не указывать явно типа элемента, например:

void sample(std::span<MyStructure> range) {
   std::ranges::uninitilized_fill(range, {"some", "arg"});

   // Раньше можно было было писать только вот так:
   std::ranges::uninitilized_fill(range, MyStructure{"some", "arg"});
}
  • В P3948 убрали лишнюю структуру std::constant_arg_t, заменив её на использование std::constant_wrapper. Другими словами, если вам нужна константа, представленная как тип, просто всегда используйте std::constant_wrapper или std::cw, вне зависимости от того, хотите ли вы держать число, строку или адрес функции.

Итоги

С++26 закончен — время заниматься C++29! И на него уже есть планы:

  • std::cstring_view / std::zstring_view,

  • profiles,

  • units,

  • pattern matching.

Если у вас есть идеи или желание помочь с воплощением полезных идей в C++29 — пишите мне или делитесь идеями на сайте РГ21 С++. Кроме того, в скором времени состоятся интересные мероприятия, связанные с C++:

Приходите, будет интересно! Кроме того, можно будет вживую пообщаться о C++ с людьми, влияющими на стандарт языка.