Pull to refresh
835.46
Яндекс
Как мы делаем Яндекс

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

Reading time8 min
Views23K

На недавней встрече комитет 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->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. Зарегистрироваться можно здесь: будут приятные сюрпризы и возможность получить ответ на волнующий вас вопрос.
Tags:
Hubs:
+80
Comments225

Articles

Information

Website
www.ya.ru
Registered
Founded
Employees
over 10,000 employees
Location
Россия