![C++23 — финал, C++26 — начало - 1 C++23 — финал, C++26 — начало - 1](https://www.pvsm.ru/images/2023/02/20/C-23-final-C-26-nachalo.jpeg)
С момента моей прошлой публикации состоялось уже две встречи международного комитета по стандартизации 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);
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 |
---|---|
|
|
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