С++23 — итоги февральской встречи международного комитета

k3ay8ji3lxa1_bpqaxlkcfsp1da.jpeg


Без лишних слов, прямо к делу — вот какие новые вкусности будут нас ждать в C++23:

  • std::expected — новый механизм сообщения об ошибках без использования исключений и без недостатков кодов возврата.
  • constexpr-математика — теперь на этапе компиляции можно доставать разные части чисел с плавающей запятой, копировать знаки и округлять числа.
  • std::ranges::to — результаты работы алгоритмов можно легко превратить в контейнер.
  • std::views::join_with — добавление разделителя между элементами.


Что мы не увидим в C++23, на что ещё можно надеяться и что ещё приняли в текущий черновик стандарта? Всё это ждёт вас под катом.

std: expected


В C++ для обработки ошибок зачастую используется подход из С — с кодами возврата:

std::errc to_int(std::string_view str, int& result) {
    const auto end = str.data() + str.size();
    auto [ptr, ec] = std::from_chars(str.data(), end, result);
    if (ec == std::errc() && ptr != end) {
        ec = std::errc::invalid_argument;
    }

    return ec;
}


У подхода есть один большой минус — код возврата можно случайно забыть проверить. А ещё его проверка неудобная, требует написания кода:

int result;
auto err = to_int(data, result); 
if (err != std::errc()) return err;
// ... продолжаем работать с result


При этом в C++ уже долгое время есть механизм обработки ошибок через исключения:

int to_int(std::string_view str) {
    int result;
    const auto end = str.data() + str.size();
    auto [ptr, ec] = std::from_chars(str.data(), end, result);
    if (ec != std::errc() || ptr != end) {
        throw std::runtime_error("failed to parse");
    }
}


И этот механизм отлично себя зарекомендовал для случаев, когда ошибки возникают редко, — если исключения не выкидывать или выкидывать нечасто, то код с исключениями работает быстрее, чем код с кодами возврата. Такую ошибку невозможно забыть обработать и продолжить работу с инвалидными данными, а код использования компактен:

const int result = to_int(data, result);
// ... продолжаем работать с result


В C++23 добавляется класс std::expected, предназначенный для замены кодов возврата более надёжным и простым в использовании механизмом:

std::expected to_int(std::string_view str) {
    int result;
    const auto end = str.data() + str.size();
    auto [ptr, ec] = std::from_chars(str.data(), end, result);
    if (ec != std::errc()) return ec;    
    if (ptr != end) return std::errc::invalid_argument;

    return result;
}


Можно пользоваться результатом как кодом возврата:

auto val = to_int(data, result);
if (!val) return val;
auto& result = *val;


А если в этом месте ошибки редки, можно вернуться к исключениям:

auto result = to_int(data, result).value(); // исключение в случае ошибки


Теперь разработчики библиотек смогут воспользоваться новым типом данных и дать возможность пользователям самим решать, как обрабатывать ошибки. Полное описание предложения доступно в P0323. Кстати, у нас в Яндексе много где используются подобные классы, они отлично себя зарекомендовали для работы с пользовательским вводом в высоконагруженных сервисах. Теперь можно будет перейти на стандартное решение.

constexpr


Поддержка вычислений на этапе компиляции в C++ не стоит на месте. В этот раз добавили constexpr unique_ptr в P2273 и constexpr в P0533. Делитесь в комментариях идеями, как можно воспользоваться этими возможностями ;-)

std: ranges: join_with


Если вы когда-нибудь программировали на Python после C++, то наверняка испытывали удовольствие от того, как легко можно собрать значения в строку с разделителем:

values = ('Hello', 'world')
print(', ',join(values)  # Выводит Hello, world


Теперь это удовольствие доступно и в C++:

for (const auto& val : {'Hello', 'world'} | std::views::join_with(", "))
    std::cout << val;  // Выводит Hello, world


Всё благодаря замечательному предложению P2441. Наконец-то можно будет заменить boost::algorithm::join стандартным и более эффективным решением.

На подходе, кстати, ещё более удобное решение:

std::map m{
    {41, "Hello"},
    {42, "world"},
};
auto s = std::format("{:m}", m); // {41: "Hello", 42: "world"}


Надеюсь, что эта идея из P2286 ещё успеет попасть в C++23.

Новые алгоритмы ranges


Добавили ещё больше алгоритмов над диапазонами:

  • std::ranges::iota(range, x) — для переопределения элементов диапазона значениями x++ (P2440).
  • std::ranges::shift_left(range, n) и std::ranges::shift_right(range, n) — для переопределения элементов диапазона значениями из того же диапазона со сдвигом n, по аналогии с std::shift_left/std::shift_right (P2440).
  • std::views::chunk(range, n) — для группировки элементов в диапазоны из n элементов (P2442).
  • std::views::slide(range, n) — группировка по n смежных элементов с шагом 1, аналог std::views::adjacent с рантайм-параметром n (P2442).
  • std::views::chunk_by(range, pred) — разделение на поддиапазоны по предикату pred (P2443).


Примеры использования новых алгоритмов:

std::vector v;
v.resize(4, 'b');                 // v == {'b', 'b', 'b', 'b'}
std::ranges::iota(v, 'm');  // v == {'m', 'n', 'o', 'p'}
std::ranges::shift_left(v, 1);  // v == {'n', 'o', 'p', 'p'}
v | std::views::chunk(2);  // {['n', 'o'], ['p', 'p']}
v | std::views::slide(2);  // {['n', 'o'], ['o', 'p'], ['p', 'p']}
v | std::views::chunk_by([](auto x, auto y) {
    return x != y;
});  // {['n', 'o', 'p'], ['p']}


std: ranges: to


До C++23 сохранить дипазон в контейнер было не самой тривиальной задачей:

auto c = std::views::iota('a', 'z') | std::views::common;
std::vector v(c.begin(), c.end());


Теперь эта задача решается в два раза проще:

auto v = std::views::iota('a', 'z') | std::ranges::to;


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

  • c.insert_range(it, r) — для вставки диапазона значений r в контейнер c по итератору it.
  • c.assign_range(r) — для перезаписи значений контейнера c диапазоном значений r.
  • c.append_range(r) — для добавления диапазона значений r в конец контейнера c.
  • c.prepend_range(r) — для добавления диапазона значений r в начало контейнера c.


Благодаря новым функциям работа с std::string упрощается:

std::string s{"world"};
std::string_view hello{"Hello "};

std::cout << s.prepend_range(hello).append_range("!!!"); // Hello world!!!


Помните, что в данный момент std::ranges::to не перемещает элементы, даже если контейнер является временной (rvalue) переменной. Поэтому для написания эффективного кода нужно использовать std::views::move (не путайте с std::move):

namespace views = std::views;
using std::ranges::to;

std::vector strs{u8"Привет", u8"дорогой читатель"};
auto s0 = strs | to; // Копирование элементов strs
auto s1 = strs | views::move | to; // Перемещение элементов strs

so.append_range(s1); // Копирование элементов s1
so.append_range(std::move(s1)); // Всё ещё копирование элементов s1
so.append_range(s1 | views::move); // Перемещение элементов s1


Все детали предложения доступны в документе P1206.

Оператор | для пользовательских ranges


Глядя на все вышеописанные новые views для ranges, невольно задаёшься вопросом: «А как нам самим написать подобный view?» P2387 старается ответить на этот вопрос и вводит для этого дополнительные утилиты:

  • std::ranges::range_adaptor_closure — базовый класс для ваших closure objects, чтобы они могли использовать предоставляемый библиотекой оператор | для комбинирования.
  • std::bind_back(f, args&&...) — вспомогательная утилита для привязывания аргументов функции с конца. Например, вызов std::bind_back(f, a3, a4)(a1, a2) превратится в f(a1, a2, a3, a4).


Но даже с этими утилитами приходится писать как минимум экран кода, чтобы сделать новый view с поддержкой оператора |. Скоро должны подоспеть новые предложения, дополнительно упрощающие написание своих view.

Прочие мелочи


  • В P0627 добавили функцию std::unreachable() для информирования компилятора о недостижимых участках кода (таких, куда программа никогда не заходит во время своего выполнения).
  • В P1413 пометили алиасы наподобие std::aligned_storage_t как deprecated и рекомендовали использовать вместо них конструкции alignas(T) std::byte t_buff[sizeof(T)];.
  • В P2255 добавили трейт для обнаружения передачи временного объекта на сохранение по ссылке. Это поможет на этапе компиляции ловить некоторые проблемы с std::tuple и std::pair.
  • В P2173 добавили синтаксис для применения атрибутов к лямбдам: [][[noreturn]]() { std::abort(); }.


Feature freeze


Комитет C++ состоит из шести основных групп:

  • Library Incubator (LEWGI);
  • Library Evolution (LEWG);
  • Library Wording (LWG);
  • Language Evolution Incubator (EWGI);
  • Core Language Evolution (EWG);
  • Core Language Wording (СWG).


Любое новое предложение должно пройти минимум через три из них: LEWGI -> LEWG -> LWG или EWGI -> EWG -> СWG. Так вот, LEWGI и EWGI прекратили работу над С++23 и начали рассматривать предложения уже только для C++26. А значит, только года через три мы увидим следующие идеи:

  • Получение stacktrace из исключений;
  • Networking;
  • Reflection;
  • Полноценная библиотека для работы с корутинами.


При этом некоторые новинки уже прошли эти подгруппы и имеют шанс оказаться в C++23:

  • mdspan;
  • flat_map;
  • float16_t;
  • generator.


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


Если у вас есть идеи, как улучшить язык C++, — пожалуйста, делитесь с нами на сайте РГ21: stdcpp.ru. C++23 всё ближу к релизу, а значит, самое время рассказать о любимых багах и недочётах языка, чтобы мы могли отправить замечания к стандарту от России.

Ну и напоследок — 17 февраля мы проведём встречу РГ21, где расскажем о готовящихся новинках C++, поделимся инсайтами и ответим на ваши вопросы. Регистрируйтесь по ссылке.

© Habrahabr.ru