13 июня закончилась встреча комитета по стандартизации C++ (также известного как WG21) в Брно (Чехия), на которой комитет работал над будущим стандартом C++, C++29. В этой статье кратко пересказаны все принятые в него нововведения с примерами их использования и ссылками на оригинальные пропозалы для тех, кто захочет познакомиться с ними детально.

1. P3596 В стандарте теперь будет перечень всего неопределенного (и заодно IFNDR) поведения. Если раньше для того, чтобы найти все неопределенное поведение, приходилось грепать по всему документу слово «undefined», то теперь в стандарте будет целый раздел «Enumeration of Core Undefined Behavior» (если вы хотите изучить его уже сейчас — то его содержание приведено по ссылке выше на 60 странице). Приобщаться к прекрасному стало легче.

2. P3097 В C++26 приняли контракты (и их даже уже реализовали в gcc 16), но с многими ограничениями, одно из которых заключалось в том, что их нельзя было использовать в виртуальных функциях. Теперь можно:

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(); // asserts g, h
  static_cast<Y*>(&z)->foo(); // asserts e, g, h, f

  X1& x1ref = z;
  X2& x2ref = z;
  x1ref.f(); // asserts a, g, h, b
  x2ref.f(); // asserts c, g, h, d
  x1ref.X1::f(); // asserts a, b

  void (X1::*pmf)() = &X1::f;
  (x1ref.*pmf)(); // asserts g, h
}

3. P3668 Также в C++29 теперь можно не реализовывать постфиксный инкремент или декремент вручную, а реализовать только префиксный и определить постфиксный как =default, и компилятор сам его реализует.

4. P2287 Ранее в C++20 из C99 притащили «Designated initializers» (существует ли общепринятый перевод на русский для этого словосочетания?), но без возможности инициализировать с помощью них поля базовых классов (см. пример ниже; он также доступен на godbolt), хотя, казалось бы?

struct A {
    int a;
};

struct B : A {
    int b;
};

int main() {
  B b1 { .b = 10 }; // Работает в C++20
  B b2 { .a = 10 }; // Не работает в C++20
  B b3 { .a = 10, .b = 5}; // Не работает в C++20
}

В C++29 эту возможность решили добавить:

struct A { int a; };

struct B : A { int b; };

B{{1}, 2}         // корректно в C++20
B{1, 2}           // корректно в C++20

B{.a=1, .b=2}     // теперь корректно в C++29
B{{.a=1}, .b=2}   // теперь корректно в C++29
B{.a{1}, .b{2}}   // теперь корректно в C++29

B{.b=2, .a=1}     // по-прежнему некорректно

5. P3091 В некоторых случаях использовать operator[] у std::(unordered_)map дико неудобно, например код ниже не скомпилируется, если объект theMap объявлен как const:

const std::map<int, double> theMap = {...} 
double largest = -std::numeric_limits<double>::infinity();
for (int i = 1; i <= 100; ++i)
  largest = std::max(largest, theMap[i]);

Без const код скомпилируется, но это может привести к нежелательному поведению, связанному с тем, что operator[] вставляет сконструированные по-умолчанию объекты, если они отсутствуют в контейнере. Чтобы обойтись без этого, можно реализовать ту же самую логику с помощью find() (да и с помощью at() тоже, но я уверяю вас, вы не хотите этого делать):

double largest = -std::numeric_limits<double>::infinity();
for (int i = 1; i <= 100; ++i)
{
  auto iter = theMap.find(i);
  if (iter != theMap.end())
    largest = std::max(largest, iter->second);
}

Но разве это красиво? Нет. И поэтому в C++29 у map, unordered_map и flat_mapтеперь есть метод lookup(), возвращающий std::optional:

constexpr double inf = std::numeric_limits<double>::infinity();

double largest = -inf;

for (int i = 1; i <= 100; ++i) {
  largest = std::max(largest, theMap.lookup(i).value_or(-inf));
}

6. P3125 Вряд‑ли кому‑нибудь требуется это часто, но кому надо — тот пусть возрадуется. Теперь можно прятать данные (в том числе в constexpr контексте) в нижних битах (не в верхних, так как это было бы непереносимо) указателей совершенно стандартно без всяких UB.

template <typename T> class maybe_owning_ptr {
  enum class ownership {
    reference,
    owning,
  };
  
  std::pointer_tag_pair<T *, ownership, 1> _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 *));

7. P3248 Стандарт определяет типы intptr_t и uinptr_t, но до сих самых пор не требовал от реализаций компиляторов их обязательное наличие — в стандарте эти типы помечены как опциональные. Теперь же их наличие считается обязательным. Сделано это по той причине, что эти типы очень полезны (да и реализованы практически во всех компиляторах практически на всех платформах) и многим авторам предложений в язык C++ хочется их использовать в API предлагаемых фичей (например, в P2835 и уже упомянутом P3125 эти типы хотели использовать), но их опциональность очень этому мешает. Теперь же они обязательны и использовать их можно с чистой совестью.

И это всё, что успел принять в стандарт комитет за встречу в Брно. Следующая будет в ноябре в Рио-де-Жанейро, и на ней комитет планирует продолжить работать над принятием различных фичей в C++29.