C++23 — финал, C++26 — начало

от автора

С момента моей прошлой публикации состоялось уже две встречи международного комитета по стандартизации C++.

Комитет занимался полировкой C++23:

  • static operator[];
  • static constexpr в constexpr-функциях;
  • безопасный range-based for;
  • взаимодействие std::print с другими консольными выводами;
  • монадический интерфейс для std::expected;
  • static_assert(false) и прочее.

И прорабатывал новые фичи C++26:

  • std::get и std::tuple_size для агрегатов;
  • #embed;
  • получение std::stacktrace из исключений;
  • stackful-корутины.

C++23

static operator[]

Прошлым летом в C++23 добавили static operator() и внедрили возможность определять operator[] для нескольких аргументов. Следующий шаг напрашивался сам собой: сделать равные возможности этим операторам, а именно — добавить возможность писать static operator[].

enum class Color { red, green, blue };  struct kEnumToStringViewBimap {   static constexpr std::string_view operator[](Color color) noexcept {     switch(color) {     case Color::red: return "red";     case Color::green: return "green";     case Color::blue: return "blue";     }   }    static constexpr Color operator[](std::string_view color) noexcept {     if (color == "red") {       return Color::red;     } else if (color == "green") {       return Color::green;     } else if (color == "blue") {       return Color::blue;     }   } };  // ... assert(kEnumToStringViewBimap{}["red"] == Color::red); 

А это точно эффективный код для преобразования строки в enum?

Может оказаться неожиданным, но этот код и правда очень эффективный. Подобным подходом пользуются разработчики компиляторов. Ну и мы во фреймворке userver подобный подход свели к отдельному классу utils::TrivialBiMap с более удобным описанием:

constexpr utils::TrivialBiMap kEnumToStringViewBimap = [](auto selector) {   return selector()       .Case("red", Color::red)       .Case("green", Color::green)       .Case("blue", Color::blue); }; 

Большая эффективность достигается благодаря особенностям работы современных оптимизирующих компиляторов, однако надо быть крайне внимательным при написании обобщённого решения. Мы готовим отдельный рассказ про этот подход — приходите на C++Russia.

Все немногочисленные детали описаны в предложении P2589R1.

static constexpr в constexpr-функциях

C++23 обзавёлся constexpr to_chars/from_chars. Однако при реализации этой новинки столкнулись с проблемой: различные массивы констант для быстрых преобразований строка<>число в некоторых стандартных библиотеках были объявлены как статические переменные внутри функций, а их нельзя использовать в constexpr-функциях. Разумеется, проблему можно обойти, но обходные пути выглядели криво.

В итоге комитет разрешил использовать static constexpr-переменные внутри constexpr-функций в P2647R1. Мелочь, а приятно.

Безопасный range-based for

Это, пожалуй, самая большая новость и радость последних двух встреч!

Но начнём с загадки. Какой баг спрятался в коде:

class SomeData {  public:   // ...   const std::vector<int>& Get() const { return data_; }  private:   std::vector<int> data_; };  SomeData Foo();  int main() {   for (int v: Foo().Get()) {     std::cout << v << ',';   } } 

Отгадка

Функция Foo() возвращает временный объект, вызов метода Get() возвращает ссылку на данные внутри этого временного объекта, а весь range based for преобразовывается в конструкцию вида:

    auto && __range = Foo().Get() ;     for (auto __begin = __range.begin(), __end = __range.end(); __begin != __end; ++__begin)     {         int v = *__begin;         std::cout << v << ',';     } 

Здесь auto && __range = Foo().Get() ; эквивалентен const std::vector<int>& __range = Foo().Get() ;. В итоге получаем висящую ссылку.

Под капотом у range based for происходит достаточно много всего, поэтому подобные баги неочевидны. Конечно, тесты с санитайзерами отлавливают такое достаточно эффективно — благо во всех современных проектах они включены и используются (мы в Яндексе не исключение). Но хотелось бы, чтобы подобные баги вообще не возникали.

Первую попытку поправить положение дел мы предприняли аж четыре года назад в РГ21 (подробности в D0890R0), но процесс заглох на этапе обсуждения. К счастью, инициативу подхватил Nicolai Josuttis и теперь в C++23 подобный код не порождает висящую ссылку: все объекты, которые создаются справа от : в range based for теперь уничтожаются только по выходу из цикла.

Технические детали можно найти в документе P2718R0.

std::print

Совсем маленькая новость: в C++23 потюнили std::print, чтобы его вывод синхронизировался с другими выводами данных. На практике для современных операционных систем ничего не изменится, но теперь в стандарте есть гарантия, что на консоль будут выводиться сообщения именно в том порядке, который задан в исходном коде:

printf("first"); std::print("второе"); 

Монадический интерфейс для std::expected

В последний момент в C++23 пролезла достаточно большая правка: для std::expected добавили монадический интерфейс по аналогии с монадическим интерфейсом для std::optional.

using std::chrono::system_clock; std::expected<system_clock, std::string> from_iso_str(std::string_view time); std::expected<formats::bson::Timestamp, std::string> to_bson(system_clock time); std::expected<int, std::string> insert_into_db(formats::bson::Timestamp time);  // Где-то в коде приложения... from_iso_str(input_data)     .and_then(&to_bson)     .and_then(&insert_into_db)     // Выкинет исключение Exception, если один из прошлых шагов завершился ошибкой     .transform_error([](std::string_view error) -> std::string_view {         throw Exception(error);     }) ; 

Полное описание всех монадических интерфейсов std::expected можно найти в документе P2505R5.

static_assert(false) и прочее

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

Так были добавлены форматеры для std::thread::id и std::stacktrace (P2693), чтобы с ними можно было работать через std::print и std::format.

std::start_lifetime_as обзавёлся дополнительными проверками времени компиляции в p2679.

static_assert(false) в шаблонных функциях перестал срабатывать без инстанцирования функции. Теперь подобный код…

template <class T> int foo() {     if constexpr (std::is_same_v<T, int>) {       return 42;     } else if constexpr (std::is_same_v<T, float>) {       return 24;     } else {       static_assert(false, "T should be an int or a float");     } } 

… компилируется и выдаёт диагностику только при условии, если передали неправильный тип данных.

Также приняли бесчисленное количество улучшений для ranges, самое крупное из которых — добавление std::views::enumerate в P2164:

#include <ranges>  constexpr std::string_view days[] = {     "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun", };  for(const auto & [index, value]: std::views::enumerate(days)) {     print("{} {} \n", index, value); } 

C++26

std::get и std::tuple_size для агрегатов

Есть одна идея по улучшению C++, которой мы уже активно пользуемся в Yandex.Go и фреймворке userver. Она доступна всем желающим благодаря Boost.PFR.

Если вы пишете обобщённую шаблонную библиотеку, то вам, скорее всего, пригодятся std::tuple и std::pair. Вот только с ними есть проблемы.

Во-первых, код с ними получается плохо читаемым: у полей нет понятных имён, поэтому сложновато догадаться, что такое std::get<0>(tuple). Возможно, пользователи вашей библиотеки не захотят работать с ними напрямую в своём коде, поэтому будут создавать объекты этих типов прямо перед вызовом ваших методов. А это может быть не эффективно из-за копирования данных.

Во-вторых, std::tuple и std::pair не пробрасывают тривиальность хранимых в них типов. Соответсвенно, при передаче и возврате std::tuple и std::pair из функций компилятор может генерировать менее эффективный код.

Описанных выше недостатков лишены агрегаты — структуры с публичными полями и без специальных функций.

Идея из P2141R0 от РГ21 как раз в том, чтобы позволить использовать агрегаты в обобщённом коде. Для этого нужно лишь сделать так, чтобы std::get и std::tuple_size работали с ними. Тогда пользователи смогут сразу передавать свои структуры в вашу обобщённую библиотеку без лишних копирований.

Идея была хорошо встречена в комитете — будем прорабатывать тесты и устранять шероховатости.

#embed

Сейчас активно идёт работа над новым стандартом языка C (без ++, тот что без классов). В новый стандарт добавляют множество полезных вещей, которые уже давно были в C++, например, nullptr, auto, constexpr, static_assert, thread_local, [[noreturn]]), так и совершенно новые для C++ фичи. Так вот: некоторые новые для C++ фичи планируется портировать из C в C++26.

Одна из таких новинок — #embed. Это препроцессорная директива для подстановки содержимого файла в качестве массива на этапе компиляции.

const std::byte icon_display_data[] = {     #embed "art.png" }; 

Осталось утрясти небольшие детали. Полное описание идеи доступно в P1967.

Получение std::stacktrace из исключений

С идеей P2370 от РГ21 нас ждал неожиданный провал.

Возможность получать стектрейс из исключения есть в большинстве языков программирования. Этот механизм весьма удобен и позволяет вместо малоинформативных ошибок Caught exception: map::at получать красивую и понятную диагностику:

Caught exception: map::at, trace:
0# get_data_from_config(std::string_view) at /home/axolm/basic.cpp:600
1# bar(std::string_view) at /home/axolm/basic.cpp:6
2# main at /home/axolm/basic.cpp:17

Особенно эта фича удобна при использовании на CI. Тогда появляется возможность сразу понять, в чём проблема в тесте, и не мучиться с попыткой повторить проблему, которая на локальной машине не воспроизводится.

Вот только международному комитету идея не зашла. Будем разбираться, что именно смутило людей и улучшать наше предложение.

Stackful-корутины

Подходит к завершению многолетний труд по добавлению базовой поддержки stackful-корутин в стандарт C++ P0876.

Мы затрагивали тему stackful- и stackless-корутин в статье«Анатомия асинхронных фреймворков в С++ и других языках», но кажется, что надо подробнее расписать плюсы и минусы.

Stackless-корутины требуют поддержки от компилятора, их невозможно реализовать своими силами в виде библиотеки. Stackful-корутины реализуются самостоятельно, например, Boost.Context.

Первые позволяют более экономно аллоцировать память. Потенциально они лучше оптимизируются компилятором, у них есть возможность быстро уничтожаться и они доступны в C++20.

Вторые намного проще внедрять в имеющиеся проекты, потому что они не требуют переписывания всего проекта на новую идиому в отличие от stackless-корутин. Фактически они полностью скрывают детали реализации от пользователя, позволяя писать простой линейный код, который под капотом будет асинхронным.

stackless stackful
auto data = co_await socket.receive(); process(data); co_await socket.send(data); co_return; // Требует, чтобы функция     // возвращала особый тип данных 

auto data = socket.receive(); process(data); socket.send(data);

P0876 уже побывал в подгруппе ядра. По итогам обсуждения было решено запретить миграции таких корутин между потоками выполнения. Основная причина такого запрета — компиляторы. Они оптимизируют доступ к TLS и кэшируют значение TLS-переменных:

thread_local int i = 0; // ... ++i; foo();  // Со stackful-корутинами может переключить поток выполнения assert(i > 0);  // Компилятор сохранил адрес в регистре, мы работаем с TLS другого потока 

Итоги

Итак, свершилось! C++23 отправлен в вышестоящие инстанции ISO, где в течение полугода будет утверждён и опубликован в виде полноценного стандарта.

А работа над C++26 идёт полным ходом! Есть неплохие шансы увидеть Executors, Networking, Pattern Matching и статическую рефлексию. Если у вас есть хорошие идеи, как сделать C++ ещё лучше, пожалуйста, делитесь ими. А ещё лучше — попробуйте написать proposal со своей идеей. Мы с радостью вам поможем!

Следить за новостями C++ и обсуждать вопросы также можно в telegram-каналах Pro.Cxx и C++ Zero Cost Conf. Ну и на сам C++ Zero Cost Conf мы уже начали отбирать доклады, приходите и расскажите как вы используете C++.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Какая функциональность важна для вас при получении std::stacktrace из исключений?
34.29% Возможность включить/выключить на райнтайме 12
45.71% Возможность полностью отключать на этапе линковки 16
57.14% Должен быть включен по умолчанию в дебаге 20
42.86% Должен быть выключен по умолчанию в релизе с возможностью включения 15
Проголосовали 35 пользователей. Воздержались 19 пользователей.

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


Комментарии

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

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