В конце ноября состоялась встреча международного комитета по стандартизации языка программирования C++.
В этот раз без внимания не остались темы:
- Рефлексия времени компиляции и оператор «монобровь»
- Constexpr, много constexpr
- SIMD
- Structured bindings as a pack
- Безопасность, контракты, libc++ hardening, профили, UB и std::launder
- Сколько бит в байте?
Рефлексия времени компиляции
Reflection, а именно предложение
P2996, на полном ходу движется к принятию в C++26.
Сейчас есть как минимум три имплементации. Одна из них
открытая. Некоторые из имплементаций доступны на https://godbolt.org/ (вот только там подустаревшие прототипы, так что современные примеры пока не собираются).
Интересный момент: текущее предложение по рефлексии не позволяет работать с атрибутами. Об этом сожалели многие разработчики (в том числе и на Хабре), так как это мешает навешивать произвольные данные на типы и поля…
UPD: Не смотря на то, что рефлексия позволяет работать с алиасами, получить их из типа не получится:
template <class T, auto name>
using JsonField = T;
JsonField<std::string, "user-name"> name{};
static_assert( // увы, не сработает :(
// `type_of` отбрасывает информацию об алиасе
[: template_arguments_of(type_of(^^name))[1] :]
== std::string_view{"user-name"});
static_assert(dealias(type_of(^^name)) == type_of(^^std::string));
И да, было решено превратить оператор для рефлексии
^
в
^^
(без пробела между символами). С лёгкой руки некоторых участников оператор назвали unibrow — «монобровь».
P2996 всю неделю варился в подгруппах LWG и CWG и там наблюдался весьма неплохой прогресс, но в черновик стандарта его пока ещё не вмерджили. При этом многие вещи, необходимые для рефлексии, были вынесены в отдельные constexpr-предложения — и их-то как раз приняли.
constexpr
Итак, теперь в compile time можно выкидывать и ловить исключения! Всё благодаря предложению
P3068. Оно, как ни странно, также улучшает и безопасность программ, потому что теперь следующий код провалится на этапе компиляции, вместо того чтобы выкинуть исключение на рантайме при использовании функции foo:
constexpr int foo(int x) {
// Невалидная дата передана в constexpr-конструктор. Конструктор теперь
// отработает на этапе компиляции, будет выброшено исключение,
// которое никто не перехватывает... Компиляция прервётся.
const Date date{"1900-23-01"};
return date.days() + x;
}
Атомарные типы
atomic<T>
и
atomic_ref<T>
тоже стали constexpr в
P3309. Теперь, например, можно писать многопоточный generic-код, и выполнять его в один поток во время компиляции:
constexpr bool process_first_unprocessed(std::atomic<size_t> & counter,
std::span<cell> subject)
{
const size_t current = counter.fetch_add(1);
if (current >= subject.size()) {
return false;
}
process(subject[current]);
return true;
}
constexpr void process_all(std::span<cell> subject, unsigned thread_count = 1) {
// ОК выполнить в compile time, если thread_count == 1
std::atomic<size_t> counter{0};
auto threads = std::vector<std::jthread>{};
assert(thread_count >= 1);
for (unsigned i = 1; i < thread_count; ++i) {
threads.emplace_back([&]{
while (process_first_unprocessed(counter, subject));
});
}
while (process_first_unprocessed(counter, subject));
}
Structured bindings также стало возможно хранить как constexpr-переменные благодаря
P2686. Теперь
constexpr static auto [v0, v1] = A{2, 3};
скомпилируется.
Алгоритмы тоже не остались без внимания: в
P3508 и
P3369 обросли constexpr алгоритмы std::uninitialized_* и std::ranges::uninitialized_*. Теперь писать свои constexpr-алгоритмы стало ещё проще!
SIMD
В черновик стандарта C++26 приняли SIMD
P1928. Другими словами, появилась возможность векторизировать обработку данных в платформонезависимом виде:
void sinuses(std::span<float> data) {
using floatv = std::simd<float>;
auto it = data.begin();
for (; it <= data.end() - floatv::size(); it += floatv::size()) {
// Прочитает сразу floatv::size() чисел, допустим 8
floatv vec(it);
// Сразу для 8 чисел посчитает синус и запишет результат обратно в data
std::sin(vec).copy_to(it);
}
for (; it < data.end(); ++it) {
*it = std::sin(*it);
}
}
В зависимости от того, под какой процессор скомпилировано ваше приложение, результат
floatv::size()
будет отличаться. Если архитектура поддерживает эффективную работу с 8 float одновременно, то будет 8. Если соберёте приложение под совершенно другую архитектуру (даже не x86), то
floatv::size()
выдаст значение именно под эту архитектуру (*).
* Если вы разрабатываетесь под архитектуру RISC-V, возможны нюансы. Пожалуйста, посмотрите насколько std::simd
хорошо ложится на вашу архитектуру, и напишите свои мысли в комментах (или мне в личку).
При работе с simd не стоит забывать про обработку последних значений, как показано в примере выше. У автора предложения была идея упростить работу с последними значениями, но пока что подобное предложение не попало в стандарт.
void sinuses(std::span<float> data) {
std::for_each(std::execution::simd, data.begin(), data.end(), [](auto& v) {
v = std::sin(v);
});
}
Ах да, std::simd тоже помечен как constexpr и им можно пользоваться в compile time.
Structured bindings as a pack
Хорошая новость! В C++26 можно будет не пользоваться Boost.PFR для поиндексного обращения к элементам, так как эта функциональность библиотеки будет доступна прямо в ядре языка C++. Благодаря
P1061 можно раскладывать кортежи и агрегаты произвольных размеров на отдельные поля, а вместе с принятым ранее
P2662 удобно к ним обращаться:
[](const auto& some_struct) {
auto [...x] = some_struct;
static_assert(sizeof...(x) < 3, "Too many fields");
if constexpr (sizeof...(x) == 2) {
foo(x...[1], x...[0]);
} else {
foo(x...);
}
}
Кстати, это предложение позволяет значительно улучшить библиотеку Boost.PFR. Можно будет обнаруживать количество полей в агрегате во время компиляции за константное время, и избавиться от некоторых проблем с детектированием. Обязательно реализую это улучшение после релиза Boost 1.87.
Прочие нововведения C++26 с последней встречи
Добавились типы
std::indirect<T>
и
std::polymorphic<T>
P3019. Первый предназначен для хранения объекта по указателю. При этом его копирование приводит к копированию объекта, на который он ссылается. Полезная штука для создания рекурсивных структур и вынесения больших подобъектов в динамическую память, чтобы сам объект мог умещаться на стеке.
std::polymorphic<T>
работает аналогично, но позволяет хранить в себе наследников от T, обеспечивая их правильное копирование.
Предложение
P3138 добавило
std::views::cache_latest
, для кеширования подсчитанных значений. Это позволяет для
transform(f) | filter(g)
кешировать результат трансформации, и не звать
f
дважды для каждого успешно прошедшего фильтрацию значения.
Ещё приземлилось множество оптимизаций для линейной алгебры и работы с многомерными спанами в
P2897,
P3222,
P3050,
P3355.
А также как всегда множество небольших багфиксов описаний в черновике стандарта — как для стандартной библиотеки, так и для ядра языка.
Безопасность
Безопасность — очень широкий термин. Многие в него вкладывают разный смысл: безопасная работа с памятью, отсутствие уязвимостей, отсутствие переполнений, отсутствие нарушений инвариантов в коде, автоматическое отслеживание времени жизни, отсутствие UB и т. п. Как ни печально, полностью валидировать правильную работу приложения в автоматическом режиме просто невозможно:
bool TriggerAirbag(control& c) noexcept {
return c.car_is_moving() || c.car_is_hit(); // ошибка
}
Код абсолютно безопасен с точки зрения любого компилятора, однако работает он небезопасно для пользователя. Из-за логической ошибки подушки безопасности сработают едва машина начнёт двигаться.
В комитете идёт активная работа по улучшению различных аспектов безопасности языка.
Первое предложение, которое уже длительное время обсуждается в комитете — контракты
P2900. Это возможность показывать инварианты вашего кода компилятору, чтобы он мог проверять время компиляции и время рантайма. Контракты подразумевают ручное проставление:
float sqrt(const float x)
pre(x >= 0);
Компилятор сможет обнаружить нарушение контракта на этапе компиляции для вызова
sqrt(-1.0)
и выдать диагностику. Для случаев, когда переменную
x
нельзя вывести на этапе компиляции, можно заставить компилятор добавлять рантайм проверку и вызывать
contract_violation_handler
.
И казалось бы, что подобные рантайм-проверки во всех частях кода должны вести к замедлению программы и пользоваться ими в продакшене невозможно. Но тут неожиданно появляется
libc++ hardening. Проставление подобных проверок для стандартной библиотеки C++ привело к замедлению на 0,3% (приложение становится медленнее на 1/333), и при этом позволило обнаружить проблемы, которые не отлавливали статические анализаторы и санитайзеры. Разработчики компиляторов поработали на славу, добавив множество оптимизаций, чтобы проверки были легковеснее и оптимизировались лучше.
Hardening очень понравился комитету: есть высокий шанс, что
P3471 попадёт в C++26 и появится hardened-режим работы стандартной библиотеки и он будет завязан на предложение по контрактам.
Отдельное предложение по улучшению безопасности кода на C++ — предложение по введению в язык профилей
P3081. Профили — это одновременно и встроенный в компилятор статический анализатор для предотвращения использования опасных конструкций (например, const_cast, аллокаций через new) и одновременно режимы для кодогенерации более надёжного кода с рантайм-проверками (например, на nullptr-разадресацию). На мой взгляд, самые слабые части этого предложения — отсутствие детального описания и отсутствие прототипа.
Есть ещё идеи по анализу времени жизни объектов на этапе компиляции, но они очень далеки от внедрения — в C++26 мы их точно не увидим.
По-прежнему идёт работа по старым направлениям. Продолжается борьба с undefined behavior (UB) в разных частях стандарта. Так, наше предложение от
Рабочей Группы 21 по уничтожению UB при работе с placement new
P3006 прошло обсуждение в EWG и есть все шансы увидеть его C++26. Для нас это предложение особо важно, так как позволяет зафиксировать текущее поведение компиляторов, например, в проекте
🐙 userver, где из-за различных их ограничений нет возможности использовать std::launder (который ни одному из компиляторов и не нужен в этом месте!).
Сколько бит в байте?
Казалось бы, простой вопрос, но есть нюанс…
Ответ
В байте CHAR_BIT бит. В 99,9% случаев, это именно 8. Но есть особые платформы, некоторые из которых до сих пор выпускаются и продаются, где в байте другое число бит. Например в TMS320C28x-процессорах в байте 16 бит.
В предложении
P3477 идёт работа над тем, чтобы зафиксировать в C++, что в байте именно 8 бит. Это немного упростит стандарт языка и позволит разработчикам не пользоваться CHAR_BIT.
А лично я думаю, что...
… не стоит закрывать дверь для 0,1% систем с нетипичными архитектурами. Более того, есть что-то заманчивое в том, чтобы позволять и дальше разработчикам железа подобные эксперименты. Например, если минимально адресуемым куском данных сделать 32 бита, то уйдут проблемы unaligned data. Так можно будет сэкономить горсть транзисторов при работе с адресами, что в свою очередь позволит экономить батарейку (кажется, в TMS320C28x как раз для этого и делали 16-бит в байте). А ведь приятно, если космический аппарат сможет пролететь на имеющейся батарейке на пару миллиардов километров больше и передать больше данных из далёкого космоса). А что вы думаете?
Итоги
C++26 уже не за горами. До feature freese остаётся всего четыре встречи комитета: сейчас уже закрывается дверь для новых предложений, одновременно затрагивающих стандартную библиотеку и ядро языка.
В ближайшем стандарте есть все шансы увидеть рефлексию, контракты и множество улучшений для безопасности (что бы в это слово ни вкладывалось).
Если у вас есть важные замечания к стандарту, пожалуйста, делитесь ими на
https://stdcpp.ru. А если хотите пообщаться по поводу placement new в userver, приходите на
ночь опенсорс-библиотек, будет весело!