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++ с людьми, влияющими на стандарт языка.

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


  1. Kelbon
    03.04.2026 07:46

     Однако его легко реализовать самостоятельно:

    можно кратче

    constexpr auto filter2 = [](auto filter) {
       return std::views::as_input | std::views::filter(std::move(filter));
    };


    1. antoshkka Автор
      03.04.2026 07:46

      Да, так ещё легче :) Можно ещё и `inline` добавить

      Авторы libstdc++ не любят использовать лямбды в header файлах, это вызывает проблемы при линковке объектов собранных разными компиляторами. Так что в примере использовал максимоально надёжный вариант, вместо самого наглядного


      1. Kelbon
        03.04.2026 07:46

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

        И лямбду можно заменить на просто функцию, если не нужно требуемого стандарту эффекта "не искать по adl"


  1. ReadOnlySadUser
    03.04.2026 07:46

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

    А проблема-то в чём?)


    1. antoshkka Автор
      03.04.2026 07:46

      Будет проезд по памяти и Segmentation Fault. Можно попробовать воспользоваться ссылкой и самостоятельно раздебажить неочевидное поведение filter. Добавление std::println в фильтр может помочь.


      1. ReadOnlySadUser
        03.04.2026 07:46

        От ответа теперь только больше вопросов:)

        По какой памяти? Что значит "проезд"? Зачем мне вообще ranges, если они такое говно?)


        1. ZirakZigil
          03.04.2026 07:46

          Ну, всякие зипы, слайды удобнее чем руками. В примерах с ренжами при этом, почему-то, всегда что-то примитивное, что без ренжей и удобнее, и сложнее сломать:

          auto nb = std::remove_if(coll1.begin(), coll1.end(), std::not_fn(large));
          std::vector sub(std::move_iterator(nb), std::move_iterator(coll1.end()));

          Хотя тут, конечно, есть нюанс с мувабельностью и известностью размера


          1. Arenoros
            03.04.2026 07:46

            я вот уже 6 год наблюдаю за этими ренжами и вот ни как не пойму чего они так вперлись, и с каких ... эти конструкции из пайпов проще и удобнее чем банальный for известный всем и в котором не нужно гадать что где и куда перемещается, копируется и т.д.
            По крайней мере во всех примерах которые показывают примеры этих самых ренжей, кроме экономии на 1% строчек в замен на доп. когднитивную нагрузку, я ни чего полезного не вижу. И это если в проект реально много тривиальных хождений по контейнерам.


            1. sergio_nsk
              03.04.2026 07:46

              Они для ленивых вычислений. Примеры на векторах - для краткости, они не показывают преимущества инетервалов.


            1. Persik1
              03.04.2026 07:46

              Ranges решают проблему композиции алгоритмов. Вместо того чтобы городить вложенные циклы или создавать временные контейнеры на каждом шаге, строишь пайплайн ленивых вычислений, а это плюс жизнь производительности потому что данные обрабатываются за один проход, но ценой читаемости


            1. X-Ray_3D
              03.04.2026 07:46

              
              Curves toCurves(const QPainterPath& pPath) {
                  using QPP = QPainterPath;
                  using El = QPP::Element;
                  Curves curves;
                  for(auto&& elements: v::iota(0, pPath.elementCount())
                          | v::transform(std::bind(&QPP::elementAt, pPath, _1))            // to Element
                          | v::chunk_by(+[](const El&, const El& r) { return r.type; })) { // to Subpath Polygons
              
                      // qInfo() << "elements" << elements.size();                         // count of elements
              
                      constexpr auto splitPaths = +[](const El&, const El& r) { return r.type > QPP::CurveToElement; };
                      auto subpaths = v::chunk_by(elements, splitPaths); // separate Curves
              
                      Curve curve;
                      for(auto&& [from, to]: v::pairwise(subpaths)) { // 'from' point to bezier Point in 'to' if
                          if(curve.empty()) curve.emplace_back(static_cast<QPointF>(from.front()));
              
                          if(to.front().type == QPP::CurveToElement) { // is arcTo
                              // Проверка, является ли кривая Безье дугой окружности
                              QPointF center;
                              double radius;
                              if(isArcOfCircle(from.back(), to[0], to[1], to[2], center, radius, 5e-3)) {
                                  // qInfo() << "Arc 1" << radius << center;
                                  curve.emplace_back(static_cast<QPointF>(to.back()), center, DIR(from.back(), center, to.back()));
                              } else {                          // is lineTo
                                  drawCross45(center, Qt::red); // debug
                                  curve.emplace_back(static_cast<QPointF>(to.back()));
                              }
                          } else
                              curve.emplace_back(static_cast<QPointF>(to.front()));
                      }
                      if(curve.size()) curves.emplace_back(std::move(curve));
                  }
                  return curves;
              }


      1. Persik1
        03.04.2026 07:46

        Проще и безопаснее написать обычный for с if внутри. Да, не так модно и не в одну строчку, зато код будет читать даже джун, а не только три с половиной человека из комитета по стандартизации


  1. SilverTrouse
    03.04.2026 07:46

    Зачем городить еще одну конструкцию std::views::safe_filter, если можно было бы просто изменить реализацию std::views::filter?


    1. antoshkka Автор
      03.04.2026 07:46

      Это бы сломало пользовательский код в валидных местах использования, например:

        auto sub = coll1 | std::views::filter(large)
                         | std::views::reverse
                         | std::ranges::to<std::vector>();


      1. ZirakZigil
        03.04.2026 07:46

        Как это вяжется с:

        Теперь общая рекомендация от комитета такова: по умолчанию перед фильтром всегда использовать std::views::as_input |. Такая конструкция уберёт лишнее кэширование из фильтра, заставит его быть гарантированно однопроходным и в целом работать без сюрпризов

        Т.е. рекомендуется по умолчанию использовать, но при этом оно в каких-то случаях что-то гарантированно сломает?


        1. antoshkka Автор
          03.04.2026 07:46

          Всё так. Рекомендуется по умолчанию использовать. Если вдруг код, с использованием аналогов safe_filter не собирается, то надо задуматься над тем, что происходит и обратить на код внимание. Если всё ещё не понятно, почему не собирается - то не стоит писать код, который вы не понимаете стоит код упростить. Если же вам всё понятно и нужен именно небезопасный фильтр - используйте его.


          1. ZirakZigil
            03.04.2026 07:46

            А, под ломается имеется ввиду поломка сборки...


          1. Mingun
            03.04.2026 07:46

            Хм. Т.е. переименовать кучу вещей и поменять возвращаемые значения некоторых функций – это сборку не ломает, и ОК, а здесь возможно что-то сломается, и уже нельзя.

            Комитет случайно не завел std::committee_bool, где можно хранить его решения?


            1. antoshkka Автор
              03.04.2026 07:46

              Менялись имена вещей, которые повились только в C++26. Так как стандарт ещё не вышел, а все реализации помечают новинки как "экспериментальные" - то менять названия ОК.

              Не ОК менять названия вещей, которые уже длительное время в стандарте (например добавились пару стандартов назад)


  1. Skirikikaka
    03.04.2026 07:46

    Чел из Яндекса не осилил привести пример из нововведения по линейной алгебре


    1. antoshkka Автор
      03.04.2026 07:46

      На этой встрече просто не было больших нововведений в линал. Примеры есть в прошлой статье, где линал непосредственно добавили https://habr.com/ru/companies/yandex/articles/801115/


  1. Persik1
    03.04.2026 07:46

    Комитет C++ собирается в Кройдоне, чтобы решить, как переименовать sat_ в saturation_. От этих решений зависит судьба высокопроизводительных вычислений во всем мире. Запомните этот коммент


  1. monah_tuk
    03.04.2026 07:46

    Поддержку корутин в std завезут? Работу с сетью?


    1. antoshkka Автор
      03.04.2026 07:46

      Сеть отложили в очередной раз, так что самое раннее в C++29 её увидим.

      Корутины уже были добавлены в C++20, в C++23 приняли std::generator. В C++26 появляется `std::execution::task`. Или вы больше ждали какие-то другие примитивы?


      1. SilverTrouse
        03.04.2026 07:46

        На счет сети, ее планируют делать на базе std::execution или будет включен в стандарт boost::asio?


        1. antoshkka Автор
          03.04.2026 07:46

          Boost.Asio уже работает с executors. Так что ответ "да" на оба вопроса


      1. Belarus
        03.04.2026 07:46

        Сеть разве не передумали добавлять в этот язык? Читал стать, што это невозможно, т.к. у сетей слишком разная реализацыя и не стандартизировать, а также обожглись на низкой производительности regex. Т.е. сторонние regex производительнее добавленной стандартной и это признано неудачей.


  1. simplepersonru
    03.04.2026 07:46

    а в рефлексию по итогу без изменений зашли пользовательские аттрибуты? Где-то раньше видел прототипы кода.


    1. antoshkka Автор
      03.04.2026 07:46

      Да, они в C++26 под именем "аннотации". Синтаксис у них похож на аттрибуты [[=any_compile_time_object]]


  1. Autochthon
    03.04.2026 07:46

    Опережающее описание для вложенных классов в каком веке появится?

    class Foo::Inner; // ошибка


    1. antoshkka Автор
      03.04.2026 07:46

      Как только вы реализуете прототип в компиляторе и напишите proposal. Дальше будет процесс обсуждения и по итогу, есть все шансы увидеть новинку в ближайшем стандарте


  1. alabamaa
    03.04.2026 07:46

    Есть предложение на C++29 добавить в стандартную библиотеку следующую функцию:

    void EarnBillionDollars ( time_point deadline, long long bankaccount );

    Undefined behavior не допускается.


  1. haqreu
    03.04.2026 07:46

    Какая короткая статья... А что там из линейной алгебры?


    1. antoshkka Автор
      03.04.2026 07:46

      В C++26 добавили функции для работы с векторами и матрицами (да-да! BLAS и немного LAPACK). Более того — новые функции работают с ExeсutionPolicy, так что можно заниматься многопоточными вычислениями функций линейной алгебры. Вся эта радость работает с std::mdspan и std::submdspan.

      В целом формат статьи - "новости с последней встречи ISO". Если хочется полный обзор C++26, то надо прочитать соответствующие статьи за последние 3 года - получится весьма подробно по всем темам.


      1. haqreu
        03.04.2026 07:46

        Спасибо. То есть, плотные векторы и матрицы в стандарте, уже неплохо.


  1. muhachev
    03.04.2026 07:46

    превратили плюсы в полигон для гиков.


    1. antoshkka Автор
      03.04.2026 07:46

      А что именно вы бы хотели изменить в C++?


      1. ReadOnlySadUser
        03.04.2026 07:46

        • Добавить сеть

        • Добавить приличные парсеры популярных текстовых форматов

        • Выкинуть всё говно, которое наворотили со строками и сделать по-человечески. Заколебало везде за собой таскать ICU.

        • Запретить устаревший синтаксис (хоть те же "эпохи" ввести)

        • Стандартизировать ABI

        • Хочется стандартный пакетный менеджер

        • Интрузивные контейнеры

        • Стандартизировать отключение исключений

        Но в целом, С++ уже ничего не поможет) Он слишком многословный и хрупкий) Его придётся любить таким, какой он есть


      1. Starl1ght
        03.04.2026 07:46

        Изкоробочные сборщик, файл проектов и пекедж менеждер. В C# все это есть, например.


  1. Fardeadok
    03.04.2026 07:46

    Каждый раз радуюсь что ушел с этого языка. Все эти симпозиумы выглядят как поиски в болоте костылей для костылей чтобы поддерживать костыли


    1. antoshkka Автор
      03.04.2026 07:46

      Вы просто не читаете аналогичные отчёты по другим языкам программирования ;-) А соответственно не знаете о проблемных местах других языков программирования и о том, какие пути обхода проблем используются там


  1. DEgITx
    03.04.2026 07:46

    Я как-то в упор не понял, смотрел на примеры с фильтрами. Написано более менее понятно, однако: там неопределенное поведение, там ошибки по памяти, там куча других проблем... И вместо того чтобы исправить само поведение (а все намекает на это), мы добавляем новую конструкцию std::views::as_input. Это же так очевидно! И все будут знать и помнить, и главное понимать что она делает.


    1. Kadzikage
      03.04.2026 07:46

      И исправить ничего нельзя ведь это сломает будущую обратную совместимость , по этому пусть будет не внятное , а через три года сделаем фикс в виде нового safe-filter . Какая то бюрократия ради бюрократии


      1. antoshkka Автор
        03.04.2026 07:46

        будущую обратную совместимость

        filter уже 6 лет с нами, аж с C++20

        А дальше тонкий вопрос - хотите ли сломать ~20% использований чтобы обнаружить 0.1%-1% UB? Комитет выбрал так не делать и идти по пути safe_filter


  1. klirichek
    03.04.2026 07:46

    Про то, чтобы запрашивать у компилятора максимальный размер стека функции - да, компилятор, возможно, что-то ответит. Но, увы, этот ответ не будет универсальным.

    Практический пример - вы узнали размер в compile time, ок.
    А потом собранное приложение запускаете, внезапно, через qemu на другой архитектуре. Например, бинарь amd64 запускаете на arm64. И там, внезапно, размер стека оказывается другим; не тем, который подсказал компилятор. Особенно заметна разница при "холодном" запуске функции. Эмулятор что-то там делает - возможно, транспилирует код под актуальную архитектуру - и этот шаг кушает стек.


    1. Quarc
      03.04.2026 07:46

      Эм, так ведь то, что вы запускаете бинарь через Qemu на какой-то левой архитектуре вовсе не проблема и не зона ответственности компилятора, формировавшего бинарь. Это явно проблема и зона ответственности любознательного экспериментатора.