C++26 — прогресс и новинки от ISO C++

Работа в комитете по стандартизации языка C++ активно кипит. Недавно состоялось очередное заседание. Как один из участников, поделюсь сегодня с Хабром свежими новостями и описанием изменений, которые планируются в С++26.

До нового стандарта C++ остаётся чуть больше года, и вот некоторые новинки, которые попали в черновик стандарта за последние две встречи:

  • запрет возврата из функции ссылок на временное значение
  • [[indeterminate]] и уменьшение количества Undefined Behavior
  • диагностика при =delete;
  • арифметика насыщения
  • линейная алгебра (да-да! BLAS и немного LAPACK)
  • индексирование variadic-параметров и шаблонов ...[42]
  • вменяемый assert(...)
  • и другие приятные мелочи

Помимо этого, вас ждут планы и прогресс комитета по большим фичам и многое другое.


Запрет возврата из функции ссылок на временное значение

Благодаря предложению , компилятор C++26 не позволит вам сформировать ссылку на временное значение, созданное в return:

const int& f1() {
    return 42;  // ошибка
}

Подобные ошибки весьма разнообразны и не всегда их легко обнаружить при беглом взгляде:

#include <map>
#include <string>

struct Y {
    std::map<std::string, int> d_map;

    const std::pair<std::string, int>& first() const {
        return *d_map.begin();  // тут возвращается std::pair<CONST std::string, int>
    }
};

При этом встроенные в современные компиляторы механизмы предупреждений зачастую могут диагностировать ещё больше неправильных использований и висящих ссылок. Так что аналоги флагов -Wall и -Wextra остаются вашими друзьями и дальше. Надёжные диагностики без ложных срабатываний продолжат потихоньку переходить в стандарт C++.

[[indeterminate]] и уменьшение Undefined Behavior

Продолжаем тему с безопасностью. В ввели понятие erroneous behavior — поведение, которое рекомендуется диагностировать компиляторам, которое запрещено в constexpr-контекстах и которое является последствием ошибки в коде. Таким поведением сделали работу с неинициализированной переменной, например:

void fill(int&);

void sample() {
  int x;
  fill(x);     // ошибочное поведение (erroneous behavior)
}

Поведение компилятора при erroneous behavior определено. Другими словами, комитет идёт к уменьшению количества Undefined Behavior в C++, чётко описывая, что происходит в том или ином случае, чтобы мотивировать компиляторы диагностировать подобные ошибки и при этом не увеличивать время выполнения приложения. Что подводит нас к новому атрибуту [[indeterminate]]. Если у нас есть неинициализированная переменная и есть функция, которая только пишет в переменную, то можно компилятору дать подсказку, что это не ошибочное поведение. Тогда значение из переменной не будут читать в функции:

void fill(int&);

void sample() {
  int x [[indeterminate]];  // без атрибута компилятор выдаст предупреждение
  fill(x);     // всё в полном порядке
}

Ошибочное поведение — это не ошибка компиляции, а предупреждение от компилятора! Фактически, многие компиляторы уже предупреждают в этом случае.

Диагностика при =delete;

Как-то раз во время обсуждения уже не помню какого предложения (кажется [[nodiscard("should have a reason")]]), мы в международной группе пришли к такой мысли: «А было бы неплохо выдавать произвольную диагностику и для =delete». Лично нам в Яндексе это очень бы пригодилось для фреймворка 🐙 userver, чтобы вместо можно было просто написать:

  const T& operator*() const& { return *Get(); }
  const T& operator*() && =delete("Don't use temporary ReadablePtr, store it to a variable");

И теперь с принятием в C++26 можно прописывать диагностические сообщения прямо в =delete.

Pack indexing

Если вы часто пользуетесь variadic templates, то вы, скорее всего, настрадались с Prolog-подобным стилем работы со списками, где приходилось откусывать по одному элементу списка с начала или конца.

Во многих шаблонных библиотеках (в том числе в стандартной библиотеке C++) реализовывали вспомогательные шаблонные механизмы для работы с variadic templates:

template <std::size_t Index, class P0, class... Pack>
struct nth {
    using type = typename nth<Index - 1, Pack...>::type;
};

template <class P0, class... Pack>
struct nth<0, P0, Pack...> {
    using type = P0;
};

template <std::size_t Index, class... Pack>
using nth_t = typename nth<Index, Pack...>::type;

Однако подобные приёмы плохо влияют на скорость компиляции кода, да и в целом их не очень приятно использовать. В комитете давно бытует мнение, что метапрограммирование должно быть похоже на обычное программирование. И если у нас есть последовательность, то очевидно должен быть способ обратиться к элементу по его индексу. Благодаря предложению , прошлый развесистый код из примера в C++26 можно просто заменить на Pack...[I]. Работает индексирование и для списка переменных:

[](auto... args) {
    assert(args...[0] != 42);
    // ...
}

Кстати, об assert

Вменяемый assert

Наверняка вы когда-то писали что-то похожее на это: assert(foo<1, 2>() == 3). И после этого получали затейливое сообщение об ошибке: error: macro "assert" passed 2 arguments, but takes just 1. Код можно поправить, если добавить дополнительные круглые скобки, от чего он красивее не становился.

С предложениями и в C23 и C++26 assert-макросы работают без лишних скобочек и телодвижений прямо из коробки.

Арифметика насыщения

Заголовочный файл <numeric> оброс дополнительными методами для работы с арифметикой насыщения:

  template<class T>
    constexpr T add_sat(T x, T y) noexcept;           // freestanding
  template<class T>
    constexpr T sub_sat(T x, T y) noexcept;           // freestanding
  template<class T>
    constexpr T mul_sat(T x, T y) noexcept;           // freestanding
  template<class T>
    constexpr T div_sat(T x, T y) noexcept;           // freestanding
  template<class T, class U>
    constexpr T saturate_cast(U x) noexcept;          // freestanding

Эти методы при переполнениях операции возвращают максимальное/минимальное число, которое может содержать определённый тип данных. Проще всего понять на примере с unsigned short:

  static_assert(std::numeric_limits<unsigned short>::max() == 65535);

  assert(std::add_sat<unsigned short>(65535, 10) == 65535);
  assert(std::sub_sat<unsigned short>(5, 10) == 0);
  assert(std::saturate_cast<unsigned short>(100000) == 65535);
  assert(std::saturate_cast<unsigned short>(-1) == 0);

Все подробности доступны в предложении .

Линейная алгебра

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

#include <linalg>

constexpr std::size_t N = 40;
constexpr std::size_t M = 20;

std::vector<double> A_vec(N*M);
std::vector<double> x_vec(M);
std::array<double, N> y_vec(N);

std::mdspan A(A_vec.data(), N, M);
std::mdspan x(x_vec.data(), M);
std::mdspan y(y_vec.data(), N);

// Заполняем значениями A, x, y.
// <...>

// y = 0.5 * y + 2 * A * x
std::linalg::matrix_vector_product(std::execution::par_unseq,
  std::linalg::scaled(2.0, A), x,
  std::linalg::scaled(0.5, y), y
);

Авторы предложения по линейной алгебре приводят таблицы как BLAS и LAPACK имена функций мапятся на C++26 имена функций из std::linalg::. В стандарте BLAS/LAPACK имена тоже доступны в виде заметок.

Также в черновик стандарта включили оптимизацию взаимодействия std::submdspan с BLAS-имплементациями в .

Атомарные fetch_max и fetch_min

Атомарные операции обзавелись методами fetch_max и fetch_min. Они нужны для атомарного вычисления максимального/минимального от текущего числа и входного параметра с последующей записью результата в атомарную переменную.

То есть в добавились атомарные операции atomic_variable_or_view = std::max(atomic_variable_or_view, x) и atomic_variable_or_view = std::max(atomic_variable_or_view, x) для std::atomic и std::atomic_view.

Мы в Яндексе уже достаточно давно пользуемся подобными функциями, например есть такая . Эти функции весьма полезны при формировании метрик работы вашего сервиса, разработке шедулеров и так далее.

Приятные мелочи

std::span обзавёлся методом at(std::size_t) и инициализацией от std::initializer_list ( и ).

Добавлен метод std::runtime_format(str) () для подставления в std::format рантайм строк формата (человекочитаемая замена для std::vformat).

В добавили std::views::concat для последовательной выдачи элементов из нескольких контейнеров:

std::vector<int> v{0, 1};
std::array a{2, 3, 4, 5};
auto s = std::views::single(6);
std::print("{}", std::views::concat(v, a, s));  //  [0, 1, 2, 3, 4, 5, 6]

Добавили конкатенацию std::string и std::string_view через оператор +. Теперь std::string{"hello"} + std::string_view{" world!"} скомпилируется ().

Благодаря , на элементы structured bindings теперь можно навешивать атрибуты: например, auto [a, b [[maybe_unused]], c] = f().

Алгоритмы, ranges и некоторые функции обзавелись возможностью работать с std::initializer_list напрямую в . Например:

struct Point { int x; int y; };

void do_something(std::vector<Point>& v) {
    std::erase(v, {3, 4});
    if (std::ranges::contains(v, {4, 2}) {
        std::fill(v.begin(), v.begin() + v.size() / 2, {42, 0});
    }
}

Если вам необходимо генерировать много случайных чисел, то добавляет замечательные функции std::generate_random. Они позволяют эффективно создавать множество чисел в ~10 раз эффективнее, чем при простом многократном вызове генератора.

Планы и прогресс по большим задачам

В комитете активно идёт работа над статической рефлексией. Больших проблем и возражений по ней нет.

Тем временем контракты опять вызвали бурные обсуждения. Предстоит подумать над тем, как уменьшить их влияние на размер итогового бинарного файла. Также предстоит сделать прототип решения и отладить его на функциях из стандартной библиотеки. Работы очень много: есть опасения, что контракты могут не успеть к C++26.

Executors чувствуют себя неплохо. Продолжается работа по вычитыванию описывающего их текста перед включением его в стандарт.

Очень приятная возможность языка вот-вот подъедет в предложении . С помощью неё можно раскладывать кортежи и агрегаты на элементы, не зная количество этих элементов:

template <class Function, class T>
decltype(auto) apply(Function&& f, T&& argument) {
    auto& [...elements] = argument;
    return std::forward<Function>(f)(std::forward_like<T>(elements)...);
}

template <class Target>
auto make_from_tuple(Tuple&& tuple_like) {
    auto& [...elements] = tuple_like;
    return Target(std::forward_like<T>(elements)...);
}

Вместе с индексированием мы получаем необычайно мощный инструмент для обобщённого программирования:

void my_function(auto aggregate) {
    auto [... elements] = aggregate;
    foo(elements...[0], elements...[1]);
    bar(elements...[2], elements...[3]); 
}

Эта функциональность покрывает большинство возможностей Boost.PFR на уровне языка. Нам в предстоит хорошенько подумать над предложением , что же из Boost.PFR имеет смысл дотащить до стандарта.

Вместо итогов

Следующая встреча международного комитета запланирована на конец июня. Если вы нашли какие-то недочёты в стандарте или у вас есть идеи по улучшению языка C++ — . Поможем советом и делом. Пользуясь случаем, хочу пригласить читателей на несколько конференций:

  • В Санкт-Петербурге состоится , где мы расскажем подробности о новинках C++ и ответим на ваши вопросы. Не забудьте зарегистрироваться.
  • Летом состоится конференция Если планируете выступать, то уже можно .
  • А уже в мае пройдёт конференция , где будет множество интересных докладов, в том числе и от нас.
  • Начался . Если вы хотели научиться написанию кода для высоконагруженных веб‑сервисов — тут вам помогут.

Источник:

Другие публикации

  • 📹
  • mobile
  • Такси

Yet another Flutter DI