Feature freeze С++23. Итоги летней встречи комитета

На недавней встрече комитет C++ «прорвало», и в черновую версию C++23 добавили:
std::mdspanstd::flat_mapstd::flat_set- freestanding
std::print("Hello {}", "world")- форматированный вывод ranges
constexprдляbitset,to_chars/from_charsstd::string::substr() &&import std;std::start_lifetime_asstatic operator()[[assume(x > 0)]];- 16- и 128-битные float
std::generator- и очень много другого
std: mdspan
После того как на прошлой встрече приняли многомерный operator[], реализация std::mdspan упростилась на порядок. И вот результат, теперь есть невладеющий тип многомерного массива:
using Extents = std::extents;
double buffer[
Extents::static_extent(0)
* Extents::static_extent(1)
* Extents::static_extent(2)
];
std::mdspan A{ buffer };
assert( 3 == A.rank() );
assert( 42 == A.extent(0) );
assert( 32 == A.extent(1) );
assert( 64 == A.extent(2) );
assert( A.size() == A.extent(0) * A.extent(1) * A.extent(2) );
assert( &A(0,0,0) == buffer );
Из коробки предусмотрена возможность работы с другими языками программирования. Так, std::mdspan третьим шаблонным параметром принимает класс-layout и есть несколько предопределённых классов:
std::layout_right— стиль расположения для C или C++, строки идут нулевым индексом,std::layout_left— стиль расположения для Фортрана или Матлаба, колонки идут нулевым индексом.
Все подробности доступны в документе P0009. Авторы обещали в ближайшее время предоставить большой набор примеров по использованию нового std::mdspan.
std: flat_map и std: flat_set
Замечательные контейнеры flat_* из Boost теперь доступны в стандарте C++. Основная фишка этих контейнеров — очень быстрая работа на небольших объёмах данных. Под капотом «плоские» контейнеры хранят данные в отсортированном массиве, что значительно уменьшает количество динамических аллокаций и улучшает локальность данных. Несмотря на сложность поиска O (log N) и, в худшем случае, сложность вставки O (N), плоские контейнеры обгоняют std::unordered_map по скорости на небольших объёмах.
Правда, в процессе стандартизации решили переделать контейнеры flat_* в адаптеры, чтобы можно было менять нижележащую имплементацию на использование своих контейнеров:
template
using MyMap = std::flat_map<
std::string, int, std::less<>,
mylib::stack_vector, mylib::stack_vector
>;
static MyMap<3> kCoolestyMapping = {
{"C", -200},
{"userver", -273},
{"C++", -273},
};
assert( kCoolestyMapping["userver"] == -273 );
const auto& keys = kCoolestyMapping.keys(); // Вдохновлено Python :)
assert( keys.back() == "userver" );
Интересный момент: в отличие от Boost-реализации, в стандарте ключи и значения контейнера лежат в разных контейнерах. Это позволяет ускорить поиски во flat-контейнерах за счёт большей локальности расположения ключей.
Полный интерфейс std::flat_set описан в документе P1222, интерфейс std::flat_map в документе P0429.
Freestanding
В стандарте C++ прописана возможность иметь такие реализации стандартной библиотеки, как hosted и freestanding. Реализация hosted требует поддержки операционной системы и обязана реализовывать все методы и классы из стандартной библиотеки. Freestanding может работать без ОС, на любой железке и не содержать часть классов и функций.
Вот только до недавнего времени не было описания freestanding, и разные производители железок предоставляли разные части стандартной библиотеки. Это усложняло портирование кода и подрывало популярность C++ в embedded-среде.
Настало время это изменить! В P1642 разметили обязательные для freestanding части стандартной библиотеки.
std: print
В C++20 внесли методы из популярной библиотеки fmt. Библиотека оказалась настолько удобной и быстрой, что её начали использовать практически везде в коде, в том числе для форматированного вывода:
std::cout << std::format("Hello, {}! You have {} mails", username, email_count);
Но у такого кода есть проблемы:
- возникнут лишние динамические аллокации,
std::coutбудет пытаться форматировать уже отформатированную строчку,- нет поддержки Юникода,
- такой код увеличивает размер результирующего бинарного файла,
- он выглядит некрасиво.
Все проблемы победили добавлением методов std::print:
std::print("Привет, {}! У вас {} писем", username, email_count);
Подробности, бенчмарки, а также возможность использовать c FILE* или стримами описаны в документе P2093.
Форматированный вывод диапазонов значений
Благодаря P2286, std::format (и std::print) обзавелись возможностью выводить диапазоны значений — вне зависимости от того, сохранены ли они в контейнер или представлены std::ranges::views::*:
std::print("{}", std::vector{1, 2, 3}); // Вывод: [1, 2, 3]
std::print("{}", std::set{1, 2, 3}); // Вывод: {1, 2, 3}
std::print("{}", std::pair{42, 16}); // Вывод: (42, 16)
std::vector v1 = {1, 2};
std::vector v2 = {'a', 'b', 'c'};
auto val = std::format("{}", std::views::zip(v1, v2)); // [(1, 'a'), (2, 'b')]
constexpr
Очень большая радость для разработчиков разных библиотек для парсинга: std::to_chars/std::from_chars теперь можно использовать на этапе компиляции для превращения текстового представления целочисленного значения в бинарное. Такая функциональность полезна и при разработке DSL. Мы в Yandex Go планируем со временем начать это использовать для проверок SQL-запросов на этапе компиляции во фреймворке userver.
std::bitset тоже стал constexpr, так что и с битами теперь можно удобно работать на этапе компиляции.
Даниил Гочаров работал над std: bitset P2417 и, вместе с Александром Караевым, над std: to_chars/std: from_chars P2291. Огромное спасибо им за проделанную работу! Обоих ребят можно найти в чатике по C++ pro.cxx и поздравить.
import std;
В стандартную библиотеку добавили первый полноценный модуль. Теперь всю библиотеку можно подключить одной строчкой import std;. Время сборки может ускориться в 11 раз (а иногда и в 40 раз!), если вместо заголовочных файлов подключить сразу весь модуль стандартной библиотеки. Бенчмарки есть в P2412.
Если вы привыкли смешивать код на C++ с кодом на C и используете C-функции из глобального namesapce, то специально для вас сделали модуль std.compat. Импортировав его, вы получите не только всё содержимое стандартной библиотеки, но и все функции из заголовочных файлов C, например ::fopen и ::isblank.
При этом сам документ P2465 на новые модули получился небольшим.
std: start_lifetime_as
Тимур Думлер и Ричард Смит сделали прекрасный подарок всем разработчикам embedded- и высоконагруженных приложений. Теперь можно делать так, и всё обязано работать:
struct ProtocolHeader {
unsigned char version;
unsigned char msg_type;
unsigned char chunks_count;
};
void ReceiveData(std::span data_from_net) {
if (data_from_net.size() < sizeof(ProtocolHeader)) throw SomeException();
const auto* header = std::start_lifetime_as(
data_from_net.data()
);
switch (header->type) {
// ...
}
}
Другими словами, без reinterpret_cast и неопределённого поведения можно конвертировать разные буферы в структуры и работать с этими структурами без копирования данных. Найти и поздравить Тимура можно всё в том же чатике по C++ pro.cxx, а полюбоваться на сам документ P2590 — здесь.
16- и 128-битные float
Стандарт C++ обзавёлся std::float16_t, std::bfloat16_t, std::float128_t и алиасами для уже существующих чисел с плавающей запятой: std::float32_t, std::float64_t.
16-битные float полезны при работе с видеокартами и в машинном обучении. Например, можно более эффективно реализовать float16.h в CatBoost. 128-битные float пригодятся для научных вычислений с большими числами.
В документе P1467 описаны макросы для проверки поддержки новых чисел компилятором, и даже есть сравнительная таблица stdfloat.properties с описанием размеров мантисс и экспонент в битах.
std: generator
Когда в стандарт C++20 принимали корутины, целились в то, что одним из вариантов их использования может быть создание «генераторов». То есть функций, которые помнят своё состояние между вызовами и возвращают новые значения, исходя из этого состояния. В C++23 добавили класс std::generator, позволяющий легко создавать свои генераторы:
std::generator fib() {
auto a = 0, b = 1;
while (true) {
co_yield std::exchange(a, std::exchange(b, a + b));
}
}
int answer_to_the_universe() {
auto rng = fib() | std::views::drop(6) | std::views::take(3);
return std::ranges::fold_left(std::move(range), 0, std::plus{});
}
В примере видно, что генераторы хорошо сочетаются с ranges. Помимо этого, как мы рассказывали на февральской встрече РГ21, std::generator эффективен и безопасен. Код, который, кажется, порождает висящую ссылку, на самом деле абсолютно валиден и не приводит к неприятностям:
std::generator greeter() {
std::size_t i = 0;
while (true) {
co_await promise::yield_value("hello " + std::to_string(++i)); // Всё OK!
}
}
Примеры, описание внутренней работы и обоснование выбранного интерфейса доступны в документе P2502.
Приятные мелочи
Стандартный класс строки обзавёлся новой перегрузкой метода substr() для временных строк: std::string::substr() &&. Код наподобие такого…
std::string StripSchema(std::string url) {
if (url.starts_with("http://")) return std::move(url).substr(5);
if (url.starts_with("https://")) return std::move(url).substr(6);
return url;
}
….теперь отработает без лишних динамических аллокаций. Подробности — в документе P2438.
Благодаря P1169 в ядре языка появилась возможность помечать operator() как static. В стандартной библиотеке подобный приём хорошо подходит для создания CPO для ranges:
namespace detail {
struct begin_cpo {
void begin() = delete;
template
requires is_array_v>
|| member_begin || adl_begin
static auto operator()(T&& val);
};
} // namespace detail
namespace ranges {
inline constexpr detail::begin_cpo begin{}; // ranges::begin(container)
} // namespace ranges
Тимур Думлер, помимо std::start_lifetime_as, отличился ещё и отличным хинтом для оптимизатора [[assume(x > 0)]]. Теперь можно давать подсказки компилятору о возможных значениях чисел и других инвариантах. Примеры и бенчмарки P1774 в некоторых кейсах показывают пятикратное сокращение числа ассемблерных инструкций.
Прочее
В стандарт также попало множество небольших правок, багфиксов и улучшений. Где-то начали использоваться move-конструкторы вместо конструкторов копирований (P2266). На радость разработчикам драйверов часть операций с volatile больше не является deprecated (P2327 с багфиксом в C++20). operator<=> стал меньше ломать старый код (P2468), юникодные символы теперь можно использовать по их имени (P2071), да и вообще все компиляторы обязали поддерживать Юникод (P2295). Добавили новые алгоритмы для ranges (ranges: contains P2302, views: as_rvalue P2446, views: repeat P2474, views: stride P1899 и ranges: fold P2322), std::format_string с целью проверки на этапе компиляции данных для std::format (P2508) и #warning (P2437). ranges научились работать с move-only-типами (P2494). И наконец, добавили std::forward_like для форварда переменной, основанного на типе другой переменной (P2445).
Итоги
Долгое время казалось, что самым значительным нововведением C++23 станет добавление std::stacktrace от РГ21 —, но на последней встрече добавили множество давно ожидаемых фич. Есть новинки и для embedded-разработчиков, и для людей, занимающихся химией/физикой/математикой/…, и для разработчиков библиотек машинного обучения, и для тех, кто делает высоконагруженные приложения.
Теперь, когда фичи C++23 зафиксированы, нам нужна ваша помощь! Если вы видите какие-то проблемы в C++23 или вам что-то сильно мешает в C++ — пишите на stdcpp.ru свои предложения по улучшению языка. Важные вещи и замечания мы закинем комментарием к стандарту, и есть все шансы, что их быстро поправят.
Кстати, мы всегда рады рассказать про новинки C++ и фичи, которые вот-вот окажутся в стандарте (например, про извлечение std::stacktrace из исключения или про std::get<1> для агрегатов).
В этот раз встреча рабочей группы 21 по итогам заседания комитета пройдёт 30 июля на конференции C++ Zero Cost Conf. Зарегистрироваться можно здесь: будут приятные сюрпризы и возможность получить ответ на волнующий вас вопрос.
