- PVSM.RU - https://www.pvsm.ru -

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

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

С момента моей прошлой публикации [1] состоялось уже две встречи международного комитета по стандартизации 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?

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

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

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

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

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

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

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

Безопасный 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 [8] (подробности в D0890R0 [9]), но процесс заглох на этапе обсуждения. К счастью, инициативу подхватил Nicolai Josuttis и теперь в C++23 подобный код не порождает висящую ссылку: все объекты, которые создаются справа от : в range based for теперь уничтожаются только по выходу из цикла.

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

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 [11].

static_assert(false) и прочее

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

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

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

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 [14]:

#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 [15]. Она доступна всем желающим благодаря Boost.PFR [16].

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

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

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

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

Идея из P2141R0 [17] от РГ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 [18].

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

С идеей P2370 [19] от РГ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 [20].

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

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

Первые позволяют более экономно аллоцировать память. Потенциально они лучше оптимизируются компилятором, у них есть возможность быстро уничтожаться и они доступны в 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++ ещё лучше, пожалуйста, делитесь ими [23]. А ещё лучше — попробуйте написать proposal со своей идеей. Мы с радостью вам поможем!

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

Автор: Antony Polukhin

Источник [27]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/programmirovanie/382936

Ссылки в тексте:

[1] моей прошлой публикации: https://habr.com/ru/company/yandex/blog/678760/

[2] разработчики компиляторов: https://llvm.org/doxygen/StringSwitch_8h_source.html

[3] фреймворке userver: https://habr.com/ru/company/yandex/blog/674902/

[4] utils::TrivialBiMap: https://userver.tech/d9/dbf/classutils_1_1TrivialBiMap.html

[5] приходите на C++Russia: https://cppconf.ru/talks/10bf97a9f8b543baa466e17a7e2ae272/?utm_source=faces&utm_medium=polukhin&utm_campaign=announce

[6] P2589R1: https://wiki.edg.com/pub/Wg21kona2022/StrawPolls/p2589r1.pdf

[7] P2647R1: https://wg21.link/p2647r1

[8] РГ21: https://stdcpp.ru/

[9] D0890R0: https://apolukhin.github.io/papers/safe_range_based_for.html

[10] P2718R0: https://wg21.link/p2718r0

[11] P2505R5: https://wg21.link/p2505r5

[12] P2693: https://wg21.link/P2693

[13] p2679: https://wg21.link/p2679

[14] P2164: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2023/p2164r9.pdf

[15] userver: https://userver.tech/

[16] Boost.PFR: https://boost.org/libs/pfr

[17] P2141R0: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2020/p2141r0.html

[18] P1967: https://wg21.link/P1967

[19] P2370: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2370r2.html

[20] P0876: https://wg21.link/P0876

[21] «Анатомия асинхронных фреймворков в С++ и других языках»: https://habr.com/ru/company/yandex/blog/647853/

[22] Boost.Context: https://www.boost.org/doc/libs/1_81_0/libs/context/doc/html/index.html

[23] делитесь ими: https://github.com/cpp-ru/ideas

[24] Pro.Cxx: https://t.me/ProCxx

[25] C++ Zero Cost Conf: https://t.me/Cxx_Zero_Cost_Conf

[26] приходите и расскажите как вы используете C++: https://cppzerocostconf.yandex.ru/

[27] Источник: https://habr.com/ru/post/715358/?utm_source=habrahabr&utm_medium=rss&utm_campaign=715358