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

от автора

На недавней встрече комитет 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::formatstd::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->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(range), 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 {     void begin() = delete;      template <typename T>         requires is_array_v<remove_reference_t<T>>             || member_begin<T> || adl_begin<T>     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. Зарегистрироваться можно здесь: будут приятные сюрпризы и возможность получить ответ на волнующий вас вопрос.


ссылка на оригинал статьи https://habr.com/ru/company/yandex/blog/678760/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *