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

в 9:00, , рубрики: C, c++, c++23, c++26, c23, compilers, exception, exception handling, iso, ranges, stacktrace, standard library, Блог компании Яндекс, Компиляторы, Программирование, С++, с++23, с++26, с23
C++23 — финал, C++26 — начало - 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?

Может оказаться неожиданным, но этот код и правда очень эффективный. Подобным подходом пользуются разработчики компиляторов. Ну и мы во фреймворке 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++.

Автор: Antony Polukhin

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js