habrahabr

C++26 — встреча ISO в Хагенберге

  • понедельник, 24 февраля 2025 г. в 00:00:07
https://habr.com/ru/companies/yandex/articles/882518/
В середине февраля в Хагенберге состоялась встреча международного комитета по стандартизации языка программирования C++.



В этот раз прорабатывались следующие большие темы:
  • std::hive
  • Constexpr, ещё больше constexpr
  • Безопасность, контракты, hardening, профили, UB и std::launder
  • Relocate
  • #embed



std::hive


В стандарт C++26 приняли контейнер, пользующийся успехом в игровой индустрии и высокочастотной торговле, так же известный под именами 'bucket array' или 'object pool'.

Идея контейнера в следующем: контейнер владеет несколькими блоками памяти с элементами и хранит логический маркер для каждого элемента, который обозначает, активен ли этот элемент или удалён — так называемое поле пропуска (skipfield). Если элемент помечен как удалённый, он пропускается при итерации.

Когда все элементы в блоке удалены, блок удаляется, чтобы итерация не теряла производительность из-за необходимости пропускать пустые блоки. Если происходит вставка, когда все блоки заполнены, выделяется новый блок памяти.

Преимущества такой структуры заключаются в следующем:
  • При удалении элементов не требуется перемещение оставшихся элементов.
  • Вставки в полный контейнер также не вызывают перемещения объектов.
  • Местоположения элементов в памяти остаются стабильными, и итераторы остаются действительными независимо от удаления или вставки.
  • Контейнер может хранить не перемещаемые и не копируемые элементы.


При этом контейнер обладает некоторыми неожиданными особенностями:
  • Контейнер неупорядоченный — вставка происходит в произвольное место контейнера.
  • Есть методы для получения итератора из указателя на элемент внутри контейнера (что весьма полезно, доказано Boost.Intrusive).
  • Из-за пропусков нет возможности за гарантированный O(1) получить доступ к N'ому элементу. Так что у контейнера нет operator[] и итераторы его не random-access, а bidirectional. Для доступа к N'ому элементу можно воспользоваться std::advance, который специализирован для контейнера и предоставляет сложность близкую к O(1).

Больше информации о контейнере, со схемами и примерами, можно найти в предложении P0447.

constexpr


Рефлексия в C++ будет происходить на этапе компиляции, и есть все шансы увидеть её в стандарте C++26. В связи с этим комитет провёл большую работу по расширению возможностей программирования в constexpr:
  • Предложение P3372 разметило практически все контейнеры стандартной библиотеки как constexpr. При этом std::hash остаётся не constexpr, так что для использования std::unordered_* контейнеров придётся предоставлять свою функцию хеширования.
  • Предложение P3378 разметило часть исключений стандартной библиотеки как constexpr.

Так же есть шансы что позволят использовать виртуальное наследование в constexpr в C++26, и constexpr корутины в C++29.

Безопасность, контракты, libc++ hardening, профили, UB и std::launder


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

Гигантский шаг в сторону увеличения безопасности — это контракты, которые приняли в C++26 на этой встрече. Они позволяют выразить «контракт по использованию и состоянию функции или класса» в виде, понятном для компилятора. Давайте воспользуемся контрактами чтобы обезопасить optional:

template <class T>
class optional {
public:
    constexpr optional() noexcept
        post( !has_value() )
    ;

    const T& operator*() const
        pre( has_value() )
    ;

    template <class... Args>
    constexpr void emplace(Args&&... args)
        post( has_value() )
    ;

    constexpr void reset()
        post( !has_value() )
    ;

    constexpr bool has_value() const noexcept;
    // ...
};

Условия, которые пишутся в pre, post и contract_assert называются предикатом контракта. Предикаты не должны менять состояние класса или параметров, не должны иметь побочных эффектов и не должны выкидывать исключений.

Указав через опции компилятора, что делать в случае нарушения предиката контракта, можно получить следующие варианты поведения:
  • enforce — в случае нарушения контракта будет вызвана функция handle_contract_violation и приложение завершит свою работу. Такой режим удобен, например, для отладочных сборок — в случае ошибки (нарушения контракта) будет выдана диагностика и приложение завершится, не давая возможность проигнорировать проблему.
  • observe — в случае нарушения контракта будет вызвана функция handle_contract_violation, и если управление будет возвращено из неё, то программа продолжит работу как ни в чём не бывало. Такой режим хорош для тестинга/pre-stable, где ошибки хочется логировать и реагировать на них, но не критичное для системы приложение может продолжать работу.
  • quick-enforce — при нарушении контракта приложение будет мгновенно завершено. Полезно для прода, если сервис работает с чувствительными данными и лучше приложение перезапустить, чем позволить ошибке влиять на дальнейшую логику.
  • ignore — контракт не проверяется и ничего не зовётся. Так же полезно для продакшн решений в закрытом контуре, крайне требовательных к производительности.

У C++26 контрактов есть пара интересных особенностей. Во первых, если компилятор обнаруживает нарушение контракта на этапе компиляции, то сборка останавливается и выдаётся диагностика. Во вторых, предикат контракта может и не вычисляться. Вместо этого его значение будет найдено с помощью хрустального шара и кофейной гущи математики на этапе компиляции! Например, для нашего optional:
optional<MyType> value;
value.emplace(42, 3.141593);  // Можно не считать предикат `post` контракта, если
                              // компилятор и так понял что optional не пустой
value->Initialize();  // Можно не считать предикат `pre` контракта,
                      // зная что optional гарантированно не пустой

Функцию handle_contract_violation можно переопределять в коде и она отлично работает с принятым в C++23 std::stacktrace предложением от Рабочей группы 21:
void handle_contract_violation(const std::contracts::contract_violation& violation) noexcept {
    std::print("Contract {} violation. Trace:\n{}", violation.comment(), std::stacktrace::current());
}


Больше деталей доступно в предложении P2900. Мои поздравления Тимуру Думлеру, участнику Рабочей Группы 21, и всем остальным авторам предложения по контрактам с принятием такого замечательного функционала в стандарт!

Так же, в C++26 приняли улучшение безопасности C++ через применение контрактов к стандартной библиотеке C++ (stadard library hardening). Другими словами, всё что мы делали вручную в примере с optional будет доступно «из коробки» для многих классов стандартной библиотеки. Подробности можно найти в P3471. Большинство стандартных библиотек как раз недавно обзавелись такими проверками, соответственно нововведение уже можно включить через специфичные макросы в старых стандартах C++, и скоро нововведение будет доступно с помощью стандартных контрактов.
А зачем это нужно, если приложение и без контрактов упадёт?
Может не упасть во многих случаях. Например optional<int>::operator* прекрасно отрабатывает на пустом optional и выдаёт мусорное значение, а санитайзеры не замечают проблему.

К другим механизмам для улучшения безопасности. С++ профили — возможность запрещать использовать некоторые конструкции языка и автоматически добавлять рантайм проверки. Такая замечательная идея, оказалась не достаточно проработанной чтобы оказаться в C++26. Но есть хорошая новость: решено было доработать и вынести идею в отдельный технический документ. Есть все шансы, что документ будет готов после завершения работы над C++26, но ещё до официальной публикации C++26 от ISO.

Помимо нового функционала для улучшения безопасности, идёт активная работа по уменьшению количества имеющихся Undefined Behavior в стандарте. В том числе, идёт работа над предложением от Рабочей группы 21 по стандартизации поведения placement new + reinterpret_cast (P3006). Предложение позволяет не использовать std::launder в случае если массив байт под placement new используется для создания объектов одного и того же типа, что делает поведение boost::optional, userver::utils::FastPimpl, V8, Clickhouse строго определённым и при этом позволяет компилятору не пессимизировать производительность из-за std::launder. Тема весьма объёмная и крайне сложная, расскажем подробности на каком-нибудь хардкорном C++ мероприятии.

Relocate


Разумеется, работа над рефлексией и безопасностью не останавливает C++ от улучшений производительности.

Когда в C++11 появилась move семантика, автор языка Бьёрн Страуструп рассказывал о нововведении на примере карандаша: «Вот у меня в левой руке карандаш. В реальном мире, чтобы переместить карандаш из левой руки в правую, мы его просто перекладываем… В C++ до move семантики, мы вынуждены были создавать в правой руке копию карандаша, и уничтожать карандаш в левой руке.»

Вот только и с C++11 с «карандашом» оставались неурядицы. После перемещения в левой руке у нас оставался «перемещённый в другое место пустой карандаш», для которого надо звать деструктор. В C++26 приняли давно ожидаемое разработчиками библиотек нововведение по релоцированию объектов. Предложение P2786 позволяет размечать классы как «тривиально перемещаемые» с помощью добавления trivially_relocatable_if_eligible после имени класса:
template <class T>
class Pencil trivially_relocatable_if_eligible {
    // ...
};

После этого можно использовать std::trivially_relocate(from_begin, from_end, to) функцию, чтобы переместить объект, завершить время жизни (lifetime) старых объектов и начать время жизни новых объектов. На практике, функция будет перемещать объекты через std::memmove, полностью избегая вызовов конструкторов и деструкторов.

Инструмент весьма специфичный, ограничения и подробности расписаны в предложении.
Почему все классы просто не сделали по умолчанию тривиально релоцируемыми?
Если класс держит внутри себя указатель на самого себя (например, некоторые реализации std::string или std::list), то он не тривиально релоцируемый.

Вторая проблема — бинарный программный интерфейс (ABI). На некоторых платформах есть возможность передавать подобные классы в функции более эффективно (через регистры), но при этом меняется ассемблер для работы с ними. Соответственно, чтобы была возможность работать быстрее, но весь существующий собранный код не разломался — потребовался отдельный «маркер» для класса.


#embed


#embed — ещё одна долгожданная новинка для C++26, позволяющая содержимое файла «зашить» в бинарник. Вот небольшой синтетический пример, на котором можно познакомиться со всеми параметрами этой новой препроцессорной директивы:
constexpr unsigned char whl[] = {
#embed "ches.glsl" \
  prefix(0xEF, 0xBB, 0xBF, ) /* префикс для вставки, если ресурс не пустой */ \
  suffix(,) /* суффикс для вставки, если ресурс не пустой */ \
  if_empty(0xBB, 0xBF, ) /* что вставить, если ресурс пустой */ \
  limit(1024*1024) /* максимальный размер для вставки */ \
  0
};


Как ни странно, это нововведение привело к ускорению компиляции. Так, например, разработчики GCC соптимизировали инициализацию больших массивов, что привело к ускорению компиляции на некоторых случаях с минут до секунд, в том числе и на старых кодовых базах.

Мы в проекте 🐙 userver тоже рады будем воспользоваться подобным механизмом, например, для встраивания SQL запросов из файлов в код и для встраивания админских/диагностических веб-страниц в сервер.

Прочие новинки


На встрече шла активная работа по улучшению std::simd. Были добавлены новые перегрузки и функции для работы с битами в P2933; некоторые конструкторы были сделаны explicit в P3430; добавлена возможность работать с комплексными числами в P2663; функция simd_split переименована в simd_chunk в P3441; функции избавлены от simd префиксов и суффиксов, вынесены в отдельный std::datapar неймспейс и импортированы в неймспейс std через using в P3287.

Также ranges обзавелись новыми алгоритмами std::ranges::reserve_hint в P2846 и std::views::to_input в P3137.

На радость embedded разработчиков в P2976 больше алгоритмов и генераторы псевдослучайных чисел разметили как freestanding (доступными в стандартной библиотеке работающей без поддержки операционной системы).

Наконец, в P3349 разрешили реализациям стандартной библиотеки превращать contiguous итераторы в простые указатели для уменьшения размера генерируемого бинарного кода и уменьшения инстанцирований шаблонов.

Итоги


C++26 всё ближе. Есть все шансы что в нём окажется ещё и рефлексия, но в нём точно не окажется pattern matching. Самое время приглядеться к черновику нового стандарта поближе, и если что-то работает не так как ожидается или что-то ломает вам разработку — пожалуйста напишите об этом в stdcpp.ru, ещё не поздно что-то исправить.

Кроме того, в скором времени состоятся:
  • C++Russia 13, 20-21 марта — будут доклады по C++, стандартизации, 🐙 userver и многое другое
  • Встреча рабочей группы 21 25 марта — расскажем о C++, ответим на вопросы. Высок шанс, что к нам сможет присоединиться (онлайн или офлайн) Тимур Думлер и из первых рук рассказать о контрактах

Приходите, будет интересно!