В середине февраля в Хагенберге состоялась встреча международного комитета по стандартизации языка программирования 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++, ответим на вопросы. Высок шанс, что к нам сможет присоединиться (онлайн или офлайн) Тимур Думлер и из первых рук рассказать о контрактах
Приходите, будет интересно!