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

в 7:08, , рубрики: c++
C++26 — прогресс и новинки от ISO C++ - 1

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

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

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

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


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

Благодаря предложению P2748, компилятор 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

Продолжаем тему с безопасностью. В P2795 ввели понятие 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");

И теперь с принятием P2573 в 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;

Однако подобные приёмы плохо влияют на скорость компиляции кода, да и в целом их не очень приятно использовать.

В комитете давно бытует мнение, что метапрограммирование должно быть похоже на обычное программирование. И если у нас есть последовательность, то очевидно должен быть способ обратиться к элементу по его индексу. Благодаря предложению P2662R3, прошлый развесистый код из примера в 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. Код можно поправить, если добавить дополнительные круглые скобки, от чего он красивее не становился.

С предложениями N2829 и P2264R7 в 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);

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

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

Свершилось! В 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
);

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

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

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

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

То есть в P0493 добавились атомарные операции 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 (P2821 и P2447).

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

В P2542 добавили 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!"} скомпилируется (P2591).

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

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

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

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

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

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

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

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

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

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 на уровне языка. Нам в Рабочей Группе 21 предстоит хорошенько подумать над предложением P2141R1, что же из Boost.PFR имеет смысл дотащить до стандарта.

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

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

Пользуясь случаем, хочу пригласить читателей на несколько конференций:

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

Автор: Antony Polukhin

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js