Привет! На связи Антон Полухин из Техплатформы Городских сервисов Яндекса. Недавно в Брно состоялась встреча международного комитета по стандартизации языка программирования C++, в которой я принимал активное участие. В этот раз началась работа над C++29 и как раз о новинках и хочется рассказать. Читать далее

ISO C++
Привет! На связи Антон Полухин из Техплатформы Городских сервисов Яндекса. Недавно в Брно состоялась встреча международного комитета по стандартизации языка программирования C++, в которой я принимал активное участие. В этот раз началась работа над C++29 и как раз о новинках и хочется рассказать:
UB и IFNDR
= default;
std::intptr_t и std::uintptr_t
floating point и std::format/std::to_chars
контракты и виртуальные функции
lookup
std::pointer_tag_pair
std::cw и строковые литералы
name_hint и stack_size_hint
Мало кто из разработчиков на C++ не слышал про злой и страшный Undefined Behavior (оно же UB, УБ, неопределённое поведение). У UB есть такой же злой брат близнец “ill formed no diagnostic required” (IFNDR). Как правило IFNDR используется когда проверку на неправильное использование C++ можно сделать уже на этапе линковки и при этом проблему очень долго/сложно диагностировать. Например, если вы напишете в 1.cpp файле функциюvoid foo(int a = 1, int b = 2), а в 2.cpp файле ту же функцию, но с другими параметрами по умолчанию void foo(int a = 2, int b = 1), то натолкнётесь на IFNDR.
Так вот, искать UB и IFNDR на страницах стандарта до C++29 было весьма неприятным занятием: мало того, что данные ужасы разбросаны по разным главам, так ещё и зачастую непонятно что надо сделать, чтобы на такую проблему натолкнуться.
В C++29 решено было собрать все явно описанные UB и IFNDR в ядре языка, и поместить их в отдельные секции стандарта. При этом сделать для них понятное описание и примеры:

UB при неправильном alignment
Новые секции специально предназначены для того, чтобы их могли читать нормальные люди, а не эксперты по языку описания стандарта языка C++. Так что прошу, приобщайтесь к UB и IFNDR.
Разумеется C++ комитет не останавливается. Если пройтись по указанным выше спискам неопределённого поведения, то можно заметить несколько недочётов или пунктов из древнейших времён, которые на современных компиляторах можно спокойно проверять на этапе компиляции. Так например в C++26 вы могли написать свой оператор delete который кидает исключения и помечен как noexcept(false):
struct X {
void operator delete(void*) noexcept(false) { throw "oops"; }
};
Однако, попытка воспользоваться им была описана как UB:
void f() {
X* x = new X();
delete x; // undefined behavior
}
В C++29 решено было убрать это неопределённое поведение, на этапе компиляции проверять и запрещать писать noexcept(false) на operator delete. Помимо этого исправления из P3424, зафиксировали поведение floating point типов в constexpr (P3899). Теперь если в процессе вычисления на этапе компилирования получится +/-Inf или +/-NaN то будет ошибка компиляции, что положительно влияет на надёжность вычислений. А ещё в P2243 убрали implementation defined поведение про объявлении шаблонных функций с extern "C", запретив такое объявление.
Если вы когда-то писали свои итераторы, то наверняка сталкивались с необходимостью писать префиксные и постфиксные операторы ++:
class Iterator {
public:
constexpr Iterator& operator++() {
++member_;
return *this;
}
constexpr Iterator operator++(int) {
auto copy{*this};
this->operator++();
return copy;
}
private:
int member_{0};
};
Если приглядеться к любой кодовой базе повнимательнее, то можно заметить что постфиксный operator++(int) в подавляющем большинстве случаев выглядит как “скопировать текущий итератор из *this, сделать префиксный инкремент для *this, вернуть копию”.
Неприятный boilerplate. Поэтому решили позволить писать = default на постфиксных операторах ++ и -- в P3668:
class Iterator {
public:
constexpr Iterator& operator++() {
++member_;
return *this;
}
constexpr Iterator operator++(int) = default;
private:
int member_{0};
};
А в P3785 применили новый функционал ко всему описанию библиотеки в стандарте.
std::intptr_t и std::uintptr_tПомимо неопределённого поведения и boilerplate в C++ есть ещё и проблемы с переносимостью кода. Так например intptr_t и uintptr_t в C++ приехали из C, и там они помечены как “опциональные”. А значит, что на платформе может их и не быть, и надо аккуратно обкладывать свой код макросами, проверять наличие этих типов и думать о замене для платформ где эти типы отсутствуют.
Вот только исследование в P3248 показало, что данные типы есть практически на всех платформах (за исключением 2-3 достаточно редких) и более того, стандартные библиотеки C++ используют эти типы без всяких проверок макросов.
В итоге, решили не усложнять жизнь пользователям, и сказать что с C++29 std::intptr_t и std::uintptr_t всегда доступны.
Смотрите какая оказия:
auto s1 = std::format("{}", 120000.0); // "120000"
auto s2 = std::format("{}", 100000.0); // "1e+05"
Вроде бы два одинаковых по длине floating point числа, однако форматируются они по разному. Такое поведение связано с тем, что под капотом используется std::to_chars, который по умолчанию старается сделать максимально короткую строчку с записью числа. Вот только подобный подход мало того что вызывает недоумение у пользователей языка, так ещё и не совпадает с поведением в других языках программирования (например с Python, под впечатлением от которого и делалась библиотека fmt, послужившая прототипом для std::format). А ещё, такой подход кушает лишнее CPU. Если не вычислять длину числа в текстовом представлении а просто задать диапазон значений для вывода числа в экспоненциальной форме, то производительность подрастает на 15%:
-----------------------------------------------------
Benchmark Time CPU Iterations
-----------------------------------------------------
normal 77.5 ns 77.5 ns 9040424
garbage 91.4 ns 91.4 ns 7675186
Теперь, с P3505, std::to_chars выводит числа более предсказуемо и приятно:
auto s1 = std::format("{}", 120000.0); // "120000"
auto s2 = std::format("{}", 100000.0); // "100000"
auto s3 = std::format("{:L}", 120000.0); // "120.000"
auto s4 = std::format("{:L}", 100000.0); // "100.000"
Контракты и виртуальные функцииВ C++26 приняли контракты - возможность проверять пред/постусловия на функциях. Более того, компилятор “видит” эти контракты и может убирать лишние проверки и даже на этапе компиляции предупреждать о проблеме. Напомню, как контракты выглядят и работают на неких псевдокодных условиях a, b, e и f:
struct X1 { void f() pre(a) post(b) {} };
struct Y : X1 { void f() pre(e) post(f) {} };
void t() {
X1 x;
Y y;
x.f(); // проверяет a, b
y.f(); // проверяет e, f
}
Вот только в C++26 запретили использовать контракты на виртуальных функциях. Ну а в C++29 в P3097 - разрешили. При этом поведение следующее:
сначала проверяется предусловие от статически известного типа
потом происходит virtual dispatch и проверяются предусловия для выбранной функции
по завершении функции выполняется постусловие
виртуальный диспатч завершается и вызываются постусловия для статического типа
Пример:
struct X1 { virtual void f() pre(a) post(b) {} };
struct X2 { virtual void f() pre(c) post(d) {} };
struct Y : X1 { void f() override pre(e) post(f) {} };
struct Z : Y, X2 { void f() override pre(g) post(h) {} };
void t() {
Z z;
z.f(); // проверяет g, h
static_cast<Y*>(&z)->f(); // проверяет e, g, h, f
X1& x1ref = z;
X2& x2ref = z;
x1ref.f(); // проверяет a, g, h, b
x2ref.f(); // проверяет c, g, h, d
x1ref.X1::f(); // проверяет a, b
void (X1::*pmf)() = &X1::f;
(x1ref.*pmf)(); // проверяет g, h
}
Последние две строчки особенно занятны: member function pointer не несёт в себе информации о контрактах с C++26, а значит “статический” контракт проверяться не будет.
lookupЕсли вы разрабатываете на C++, то раз в недельку да пишете что-то наподобие:
auto do_something(int key, const std::unordered_map<int, int>& cache) {
int result = 42;
if (auto it = cache.find(key); it != cache.end()) {
result = it->second;
}
return result;
}
Казалось бы, простая задача “если нет значения по ключу то верни дефолт”, но вот решается она в неприличное количество строк кода, да при том с итераторами.
В P3091 решили покончить с этим безобразием и добавили во все ассоциативные контейнеры методы std::optional<Value&> lookup(const Key&). С ними код становится приятным для чтения и написания:
auto do_something(int key, const std::unordered_map<int, int>& cache) {
return cache.lookup(key).value_or(42);
}
std::optional обладает монадическими методами и удовлетворяет концепту диапазона, так что можно писать и более хитроумные конструкции:
auto do_something(int key, const std::unordered_map<int, int>& cache) {
return cache.lookup(key)
.or_else([&cache] { return cache.lookup(kFallbackValue); })
| std::views::transform(...)
;
}
std::pointer_tag_pairP3125 привнёс в стандарт С++29 возможность тегировать указатели, в том числе и на этапе компиляции:
template <typename T>
class maybe_owning_ptr {
enum class ownership: unsigned {
reference,
owning,
};
std::pointer_tag_pair<T *, 1, ownership> ptr_;
public:
constexpr maybe_owning_ptr(T* && pointer) noexcept
: ptr_{pointer, ownership::owning} { }
constexpr maybe_owning_ptr(T & ref) noexcept
: ptr_{&ref, ownership::reference} { }
constexpr decltype(auto) operator*() const noexcept {
return *ptr_.pointer();
}
constexpr T * operator->() const noexcept {
return ptr_.pointer();
}
constexpr ~maybe_owning_ptr() noexcept {
if (ptr_.tag() == ownership::owning) {
delete ptr_.pointer();
}
}
};
static_assert(sizeof(maybe_owning_ptr<int>) == sizeof(int *));
Учтите что std::pointer_tag_pair использует только наименее значащие биты для хранения тегов. Использование старших битов считается непереносимым, и решено было пока их не трогать.
В C++26 есть универсальный тип “константа времени компиляции” - std::constant_wrapper. Подобные типы в C++ не редкость, так уже давно имелся std::integral_constant и std::true_type/std::false_type. Так вот, в последний момент в std::constant_wrapper добавили правку, позволяющую использоваться std::cw со строковыми литералами:
auto a = std::cw<"foo">;
auto b = std::cw<"bar">;
Вот только с этой правкой код
template <int I>
void f(std::constant_wrapper<I>) {
// ...
}
int main() {
f(std::cw<5>);
}
перестал собираться:
<source>:9:6: error: no matching function for call to
'f(conststd::constant_wrapper<std::_CwFixedValue<int>{5}, int>&)'
candidate 1: 'template<int I> void f(std::constant_wrapper<((std::_CwFixedValue<int>)I)>)'
template argument deduction/substitution failed:
mismatched types 'int' and 'const std::_CwFixedValue<int>'
Более того, ничего полезного со строковыми литералами в std::constant_wrapper тоже сделать нельзя:
auto a = std::cw<"foo">;
auto b = std::cw<"bar">;
a == b; // ошибка компиляции
В связи с этим в P4206 правку откатили (в том числе откатили и для C++26). Удобной работой со строками во время компиляции продолжат заниматься в P0424, P3380 и P3554.
name_hint и stack_size_hintP0424 позволил задавать имя для потока ОС и выставлять размер стека потока:
#include <thread>
void f(int);
int main() {
std::thread thread( // для std::jthread тоже работает:
std::thread::name_hint("fs-worker"),
std::thread::stack_size_hint(512*1024),
f, 42);
// ...
thread.join();
}
Теперь если вы через утилиту top выведете имена потоков для написанного выше приложения, то увидите “fs-worker”. А если приложение упадёт, то в core file тоже будет указано имя потока, что ощутимо упрощает диагностику проблем (мы в Техплатформе Городских сервисов Яндекса давно пользуемся подобным механизмом и в 🐙 userver все потоки имеют понятные имена).
P0424 позволил использовать индексирование для пака шаблонных шаблонов:
template < template <typename> typename... TT>
struct S {
template <typename T>
using First = TT...[0]<T>;
};
P3428 добавил возможностей для батчевой работы с hazard pointer.
Как всегда приземлились новые улучшения в ranges в P3052, в simd в P3319 и в P3772, в mdspan в P3242.
Ну и наконец в P3104 приземлились новые операции для работы с битами:
bit_reverse(uint32_t{0x00001234}) // 0x24c80000
bit_repeat(uint32_t{0xc}, 4) // 0xcccccccc
uint32_t x = /* ... */; // a b c d
uint32_t m = 0b0101; // 0 1 0 1
uint32_t z = bit_compress(x, m); // 0 0 b d
uint32_t x = /* ... */; // a b c d
uint32_t m = 0b0101; // 0 1 0 1
uint32_t z = bit_expand(x, m); // 0 c 0 d
ИтогиДо выхода C++29 ещё 3 года, работа вовсю кипит! Если у вас есть хорошие идеи, как сделать C++ лучше - то смело несите их в stdcpp.ru. А ещё лучше - беритесь за интересные идеи, пишите proposal и мы поможем вам донести идеи до международного комитета по стандартизации C++.
P.S.: Если кто был на Back2Back конференции, то эта статья может показаться вам знакомой. Всё потому, что мы рассказываем о новостях C++, Boost и userver не только на Хабре, но и практически на всех C++ конференциях. Ну а если вы вдруг никогда не ходили на конференции - стоит хотя бы разочек попробовать, вдруг понравится :)
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Худший язык программирования всех времён /s | -2 | 6 | 01-07-2026 |
| 2 | Шаблоны C++ как инструмент архитектуры: compile-time dispatch, type traits и type erasure | 5 | 7 | 28-06-2026 |
| 3 | How to create a standard, 2026 addition | 0 | 7.03 | 04-08-2026 |
| 4 | "Союз МС-29" стартовал к МКС с Байконура - Интерфакс | 0 | 5 | 14-07-2026 |
| 5 | "Союз МС-29" стартовал к МКС с Байконура - Интерфакс | 0 | 5 | 14-07-2026 |
| 6 | «Союз МС-29» с новым международным экипажем пристыковался к МКС — подробности события ... | 0 | 5 | 14-07-2026 |
| 7 | Для полноценного импортозамещения вендоры, заказчики и государство должны объединиться | 0 | 6 | 02-07-2026 |
| 8 | В Нижнем Новгороде подвели итоги первого дня конференции ЦИПР-2023 | 0 | 0 | 01-06-2023 |
| 9 | Лавров встречается с генсеком ШОС | 0 | 0 | 15-01-2020 |