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 и uintptr_t, но до сих самых пор не требовал от реализаций компиляторов их обязательное наличие — в стандарте эти типы помечены как опциональные. Теперь же их наличие считается обязательным. Сделано это по той причине, что эти типы очень полезны (да и реализованы практически во всех компиляторах практически на всех платформах) и многим авторам предложений в язык C++ хочется их использовать в API предлагаемых фичей (например, в P2835 и уже упомянутом P3125 эти типы хотели использовать), но их опциональность очень этому мешает. Теперь же они обязательны и использовать их можно с чистой совестью.

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

Комментарии (10)


  1. n0lavar
    22.06.2026 13:42

    Новая фича «Designated initializers» - топ. Столько раз об это спотыкался.

    Тем временем MSVC
    Тем временем MSVC


    1. Kripiron
      22.06.2026 13:42

      MSVC создали лучшую реализацию модулей и хватит)


      1. domix32
        22.06.2026 13:42

        А почему она лучшая?


  1. domix32
    22.06.2026 13:42

    struct X1 { virtual void f() pre(a) post(b); };

    а объявления контрактов прибиты к объявлению функции или их можно в месте имплементации писать? типа

    // header.hpp
    struct X1 { virtual void f(); };
    
    // header.cpp 
    #include "header.hpp"
    
    void X1::f()
      pre(a) post(b)
    {
      /*implementaion */
    }


    1. eoanermine Автор
      22.06.2026 13:42

      Увы, в месте имплементации так писать не получится. Согласно P2900, в котором были введены контракты:

      Any function declaration is a first declaration if no other declarations of the same function are reachable from that declaration; otherwise, it is a redeclaration. The sequence of function contract specifiers on a first declaration of a function introduces the corresponding function contract assertions that apply to that function. It is ill-formed, no diagnostic required (IFNDR) if multiple first declarations for the same function are in different translation units that do not have the same sequence of function contract specifiers. A redeclaration of a function shall have either no function contract specifiers or the same sequence of function contract specifiers as any first declaration reachable from it; otherwise, the program is ill-formed.

      А этот пропозал, разрешающий использование контрактов в виртуальных функциях, это положение дел не меняет.


    1. Kelbon
      22.06.2026 13:42

      это противоречит смыслу контрактов


  1. Vobraz
    22.06.2026 13:42

    Можете раскрыть, почему бы не использовать at() для const std::map в вашем примере? Я понимаю, что он может бросить исключение, но допустим что готов к этому. А накладные расходы на проверку валидности внутри .at() кажется такие же как и у iter != theMap.end()


    1. eoanermine Автор
      22.06.2026 13:42

      Вы правы, что можно использовать at(), в пропозале даже представлен пример с его использованием:

      double largest = -std::numeric_limits<double>::infinity();
      for (int i = 1; i <= 100; ++i)
      {
        try {
          largest = std::max(largest, theMap.at(i));
        } catch (const std::out_of_range&) { }
      }
      

      Часть с «я уверяю вас, вы не хотите этого делать» я добавил как раз из-за необходимости обработки исключений в этом случае: практически на пустом месте получаем ухудшение производительности.


  1. rbdr
    22.06.2026 13:42

    Писал на С++ 20 лет, сильно радовался ренессансу в виде 11, 17 и 20.

    Но вот свои последние проекты уже пишу только на зиге, когда есть нужда в быстром и портируемом коде. Не дождался я рефлексии и пр - стандарты утверждают, но реализаций - нет. А Зиг уже просто язык нового уровня и на нем что-то делать - одно удовольствие.


    1. domix32
      22.06.2026 13:42

      Только есть нюанс в виде отсутвия 1.0 версии и шансу получить ломающие изменения вместе с условным apt install zig. Для стартаперов/фрилансеров наверное хорошо, для корпораций - без стандарта, без гарантий совместимости хотя бы лет на 5 - не очень.