
На недавней встрече комитет C++ «прорвало», и в черновую версию C++23 добавили:
std::mdspan
std::flat_map
std::flat_set
- freestanding
std::print("Hello {}", "world")
- форматированный вывод ranges
constexpr
дляbitset
,to_chars/from_chars
std::string::substr() &&
import std;
std::start_lifetime_as
static operator()
[[assume(x > 0)]];
- 16- и 128-битные float
std::generator
- и очень много другого
std::mdspan
После того как на прошлой встрече приняли многомерный
operator[]
, реализация std::mdspan
упростилась на порядок. И вот результат, теперь есть невладеющий тип многомерного массива: using Extents = std::extents<std::size_t, 42, 32, 64>;
double buffer[
Extents::static_extent(0)
* Extents::static_extent(1)
* Extents::static_extent(2)
];
std::mdspan<double, Extents> 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 <std::size_t N>
using MyMap = std::flat_map<
std::string, int, std::less<>,
mylib::stack_vector<std::string, N>, mylib::stack_vector<int, N>
>;
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<int>{1, 2, 3}); // Вывод: [1, 2, 3]
std::print("{}", std::set<int>{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<std::byte> data_from_net) {
if (data_from_net.size() < sizeof(ProtocolHeader)) throw SomeException();
const auto* header = std::start_lifetime_as<ProtocolHeader>(
data_from_net.data()
);
switch (header->msg_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<int> 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(rng), 0, std::plus{});
}
В примере видно, что генераторы хорошо сочетаются с ranges. Помимо этого, как мы рассказывали на февральской встрече РГ21,
std::generator
эффективен и безопасен. Код, который, кажется, порождает висящую ссылку, на самом деле абсолютно валиден и не приводит к неприятностям:std::generator<const std::string&> 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 {
template <typename T>
requires is_array_v<remove_reference_t<T>>
|| member_begin<T> || adl_begin<T>
static auto operator()(T&& val);
};
void begin() = delete; // poison pill
} // 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. Зарегистрироваться можно здесь: будут приятные сюрпризы и возможность получить ответ на волнующий вас вопрос.