ISO C++
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


UB и IFNDR

Мало кто из разработчиков на 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
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", запретив такое объявление.

= default

Если вы когда-то писали свои итераторы, то наверняка сталкивались с необходимостью писать префиксные и постфиксные операторы ++:

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 всегда доступны.

floating point и std::format/std::to_chars

Смотрите какая оказия:

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_pair

P3125 привнёс в стандарт С++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 использует только наименее значащие биты для хранения тегов. Использование старших битов считается непереносимым, и решено было пока их не трогать.

std::cw и строковые литералы

В 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_hint

P0424 позволил задавать имя для потока ОС и выставлять размер стека потока:

#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++ конференциях. Ну а если вы вдруг никогда не ходили на конференции - стоит хотя бы разочек попробовать, вдруг понравится :)

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


  1. Playa
    17.08.2026 07:49

    z.f();                     // проверяет g, h
    static_cast<Y*>(&z)->f();  // проверяет e, g, h, f
    

    Какой-то вынос мозга если честно. Почему так?

    auto do_something(int key, const std::unordered_map<int, int>& cache) {
        return cache.lookup(key).value_or(42);
    }
    

    Зачем в данном случае нужна эта прослойка с std::optional, если всё равно копируем значение? cache.lookup(key, 42) было бы ещё проще.


    1. antoshkka Автор
      17.08.2026 07:49

      Какой-то вынос мозга если честно. Почему так?

      Так получается более логично, чем в остальных случаях. Смотрите, давайте сделаем классический non virtual interface:

      struct Base {
        void call_me(int x) pre(x > 0) { call_me_impl(x); }
      
      private:
        virtual void call_me_impl(int x) = 0;
      };
      
      struct Derived {
        void call_me_impl(int x) override pre(x >= 0);
      };

      Если позвать `derived.call_me_impl(0);`, то всё должно отработать нормально - просто зовётся функция класса, надо на ней проверить `pre(x >= 0)`.
      Когда вызов будет происходить через `call_me` базового класса, то надо проверить контракт на `call_me`, а потом контракт `Derived::call_me_impl`

      С non virtual interface всё крайне логично. Вот только этот паттерн немного громоздкий (зачастую надо дополнительную функцию писать для каждой виртуальной) и люди его сокращают до:

      struct Base {
        virtual void call_me(int x) pre(x > 0) = 0;
      };
      
      struct Derived {
        void call_me(int x) override pre(x >= 0);
      };

      Поведение получаем то же, что и с non virtual interface


  1. Janycz
    17.08.2026 07:49

    Теперь если в процессе вычисления на этапе компилирования получится +/-Inf или +/-NaN то будет ошибка компиляции, что положительно влияет на надёжность вычислений.

    А если нужен NaN с нагрузкой?


    1. antoshkka Автор
      17.08.2026 07:49

      Для получения Nan и Inf в constexpr можно пользоваться `std::numeric_limits<T>::quiet_NaN()` и `std::numeric_limits<T>::infinity()`. Умножать эти значения на -1 или просто использовать унарный оператор `-` всё ещё можно

      А каким образом сейчас вы задаёте payload для NaN?


      1. Janycz
        17.08.2026 07:49

        Каноничный способ это std::nan из <cmath>, но он не constexpr. UB-реализации (которые тем не менее работают) через union и reinterpret_cast тоже не constexpr. Использование memcpy тоже не constexpr. В g++ для этого есть __builtin_nan, который, по сути, constexpr (но не всегда). Надо признать, что сейчас адекватного способа для этого нет. Более того, __builtin_nan и std::nan принимают на вход строку, а std::to_string (и другие функции преобразования) не constexpr.

        Но, предлагается унифицированное поведение, которое ухудшает текущее поведение. Из прочитанного предложения по ссылке предлагается сделать любое выражение constexpr double d = expression невалидным, если expression возвращает неопределенность. Это также относиться также к std::numeric_limits<T>::quiet_NaN() и подобным, что они constexpr сейчас, но предлагается сделать не constexpr.


        1. antoshkka Автор
          17.08.2026 07:49

          Каноничный способ это std::nan

          Если готовы побороться за payload в nan, то с радостью помогу вам. Но вам придётся проработать основные моменты и написать proposal. Подозреваю что там будут трудности, кажется некоторые платформы не позовляют делать -nan, а что у них с payload для nan не представляю.

          Но, предлагается унифицированное поведение, которое ухудшает текущее поведение

          На самом деле всё хорошо, стандартизировали текущее поведение GCC. Там даже есть достаточно понятные примеры:

          constexpr std::float32_t min = std::numeric_limits<std::float32_t>::min();          // OK
          constexpr std::float32_t max = std::numeric_limits<std::float32_t>::max();          // OK
          constexpr std::float32_t inf = std::numeric_limits<std::float32_t>::infinity();     // OK
          constexpr std::float32_t nan = std::numeric_limits<std::float32_t>::quiet_NaN();    // OK
          
          constexpr std::float32_t inf2 = inf * 2;    // OK, also positive infinity
          constexpr std::float32_t zero = min / max;  // OK, result cannot be represented, and is rounded to zero
          constexpr std::float32_t oflo = max * 2;    // error: non-finite result but operands are finite ([expr.const.core])
          constexpr std::float32_t nan2 = nan * 2;    // OK, propagating a NaN
          constexpr std::float32_t udef = inf * 0;    // error: result is NaN but neither operand is NaN ([expr.const.core])
          constexpr std::float32_t div0 = max / 0;    // error: division by zero is undefined ([expr.mul], [expr.const.core])


          То есть всё работает ожидаемо, NaN и Inf можно получать в compile time из numeric_limits, но нельзя "случайно" их создать выражением без NaN/Inf


          1. Janycz
            17.08.2026 07:49

            Странно, вот они привели пример, указанный вами как пример того, что должно быть, но при этом пишут в п. 3.6:

            The current wording in [expr.pre] paragraph 4 is clear that any expression that produces NaN has undefined behavior. NaN is neither mathematically defined nor is it defined to be in the range of representable values (even intuitively, it would have to be outside any range).

            However, the standard library doesn't seem to care about this, considering that numeric_limits<T>::quiet_NaN and numeric_limits<T>::signaling_NaN have been marked constexpr

            И как такое понимать? Отсюда непонятно, что они, согласно 3.6, считают верным поведением. И только вот пример это поясняет.

            Если готовы побороться за payload в nan, то с радостью помогу вам. Но вам придётся проработать основные моменты и написать proposal. Подозреваю что там будут трудности, кажется некоторые платформы не позовляют делать -nan, а что у них с payload для nan не представляю.

            Есть std::numeric_limits<T>::is_iec559 -- если он истинный, то nan будет. Я вот также удивлен, что std::to_string не constexpr, хотя сейчас конструкторы в std::string и operator+ для std::string являются constexpr.


  1. SilverTrouse
    17.08.2026 07:49

    Развитие рефлексии планируется? Все еще хотелось бы видеть token injection , к примеру на базе https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p3294r2.html


    1. antoshkka Автор
      17.08.2026 07:49

      Работа над P3294 продолжается, пока непонятно, получится ли увидеть в C++29


  1. domix32
    17.08.2026 07:49

    auto s3 = std::format("{:L}", 120000.0); // "120.000"

    что-то не очень понял почему такой вывод. Оно десятичный разделитель по-умолчанию в точку превратило?

    std::pointer_tag_pair

    прочитал мотивацию, но так и не понял какой у них юзкейс. HAMT в std вроде так и не появилось ни в каком виде, альтернативных аллокаторов вроде тоже. Заготовка для hazard pointers? Кстати слышно ли что-то про стандартные каналы ?


    1. antoshkka Автор
      17.08.2026 07:49

      почему такой вывод

      `"{:L}"` выводит с использованием текущей локали. В примере стоит локаль с разделителем `.` между тысячными. Обычный `"{}"` выводит без использования локалей, и будет просто "120000"

      какой у них юзкейс

      Тут как вам подскажет фантазия. Весьма полезная штука при создании своих деревьев, списков и прочих контейнеров


  1. Daddy_Cool
    17.08.2026 07:49

    Джентльмены, а есть ли какая-нибудь книжка которая рассказывает зачем нужны разные фичи С++?
    Т.е. вот я знаю Си, и умею программировать в стиле 70-х, ну там - загрузить данные, посчитать, выгрузить. Могу что-то на BC++Builder "нарисовать", но вот дальше... Когда умеешь что-то низкоуровнево (да, я еще слегка умею на асме) возникает вопрос "а нафига нам это всё в зоопарке", это некая ментальная ловушка, зачем учиться управлять экскаватором если можно лопатой выкопать яму (и возможно получится быстрее, если задача разовая).
    Хочется примеров - типа вот вам надо это (а надо потому-то и потому-то) и вот как это легко и изящно решается средствами языка.


    1. domix32
      17.08.2026 07:49

      У пропозалов обычно есть секция про мотивацию, правда не сказать что она везде достаточно подробная чтобы полноценно объяснить её.


    1. eao197
      17.08.2026 07:49

      Вам когда-нибудь приходилось использовать std::map или std::set?


    1. naky
      17.08.2026 07:49

      Джентльмены, а есть ли какая-нибудь книжка которая рассказывает зачем нужны разные фичи С++?

      В принципе любая. Например, Beginning c++26, Ivor Horton, Peter Van Weert.

      За простыми примерами использования фич можно заглянуть в примеры на https://en.cppreference.com/


    1. Jijiki
      17.08.2026 07:49

      если знаете С вы не ограничены, ставьте при компиляции 23/26 стандарт, используйте структуры, деструкторы, классы, лямбды, помните в С управление памятью например в текстовом редакторе? так вот это можно сварганить на С++, или взяв std::vector, или написав свою обертку дженерика по принципу того управления памятью со всеми вытекающими, по сути С++ в базе даёт просто удобство использования стандартных контейнеров и возможность создания своих контейнеров, вопрос для досуга(напишите функцию на С++, которая понимает где const char[] а где const char*), вы задаёте вопрос по-сути правильный тут главное точно понять зачем мы изобретаем всё заново и для чего.... в С++ всё для этого есть, разве что ждём Send/Receive или качаем либу от Nvidia или еще кого )

      отвечая на вопрос, С++ имеет возможность уйти от никоуровневости за счет абстракций/библиотек, или можно написать проект снизу вверх, замиксив асм/симд/С/фишки в С++, которые есть в стандартной библиотеке, например всё теже маллок и тп, так же есть возможность заюзать другие аллокаторы mimalloc jemalloc..., это как фишки зиг, посмотрите язык на досуге, какие там фишки(компил тайм, аллокаторы, симд из коробки удобный, синтаксис визуально сишный, свой билдер, так же можно билдить С/С++ проекты. посмотрите может вам этого достаточно будет)

      получается у С++ фишки теже, лямбды - анонимные классы, классы/структуры деструкторы фундамент по-сути наверно, есть констекспр тоже, можно написать свои итераторы, есть концепты, конструкторы/операторы перемещения/копирования, возможность обобщенного кода, этого поидее за глаза хватает...

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

      а в эпоху где всё перепроверять и zero trust - это практически нереальная миссия....


    1. Siemargl
      17.08.2026 07:49

      Ну например Мейерса. Как раз как апгрейд курс до С++14. (С++17 не сильно улучшен)

      Староватая краткая аннотация.

      Ну а про С++20 и новее еще не смотрел и сам, там очередной виток ада и содомии =)


  1. ImagineTables
    17.08.2026 07:49

    Заменить const на mut? Нет.
    Заставить при наличии [[nodiscard]] обрабатывать КАЖДЫЙ std::unexpected? Нет.
    Дать из constexpr безопасный доступ к памяти и диску? Нет.
    Запретить noexcept(false) для деструкторов? ДА!!!

    P.S. std::format, как минимум под VC, тащит за собой все кишки локалей дат, и одна строка std::format(L"{}", 5) раздувает бинарь на сотни килобайт в релизе. В то время как прототип (ц) как-то умеет собираться модульно и размер увеличивается на столько, на сколько ты его юзаешь. Я бы начал улучшать std::format с этого. А продолжил бы поддержкой именованных аргументов в компайл-тайме. Вот чего действительно не хватает. А кто пишет пустые брэкеты {}, а потом удивляется непредсказуемому результату, тот сам себе злобный буратино.


    1. antoshkka Автор
      17.08.2026 07:49

      std::format, как минимум под VC, тащит за собой все кишки локалей дат, и одна строка std::format(L"{}", 5) раздувает бинарь на сотни килобайт в релизе

      Да, такая же история и с libstdc++ от GCC. Реализации стандартной библиотеки пока помещают всю имплементацию в заголовочный файл, чтобы не думать о ABI и его сломе. Когда реализации устаканятся и дооптимизируются, то они будут внесены в cpp. В новом libstdc++ от GCC даже есть уже нужный макрос сборки, но по умолчанию пока всё в заголовочных файлах


    1. Janycz
      17.08.2026 07:49

      Заставить при наличии [[nodiscard]] обрабатывать КАЖДЫЙ std::unexpected? Нет.

      Это невозможно в силу определения класса std::expected. Как обработать каждый std::expected<T, E>, где E идейно бесконечен, например, строка или вектор (как бы в силу ограничения возможного максимального размера памяти, E, на практике, конечен)?


      1. ImagineTables
        17.08.2026 07:49

        Невозможно составить список всех std::unexpected’ов, возвращаемых функцией?


        1. Janycz
          17.08.2026 07:49

          Вообще говоря, да. В частности, если у вас есть библиотека без исходного кода, где есть только .lib/.a + .h.

          Еще можно привести вот такой пример: [[nodiscard]] std::expected<std::string, Utf8DecodeError> utf8_to_cp1251(std::u8string_view sv). Мы получаем либо корректную строку в кодировке Windows-1251, или экземпляр класса Utf8DecodeError, который содержит в качестве одно из полей строку std::string -- то, что получилось раскодировать (например, где все непредставимые символы заменены на '?' или строку до первого непредставимого символа -- дизаин здесь может быть разным). При такой реализации составить список всех std::unexpected невозможно.


          1. ImagineTables
            17.08.2026 07:49

            Есть два варианта: считать, что это пересечение границы unsafe-safe, и не делать ничего, либо сделать из .lib подобие crate’а, включив в него всю необходимую для безопасной компиляции информацию.

            В Rust’е если ты не обработал все возможные ошибки, код не скомпилируется. И исключения не нужны. Было бы здорово иметь в C++ возможность писать так же, и насколько я понимаю, std::expected это как раз шаг в нужном направлении. Но если его добавили в C++23, то в C++29 я имел ожидания, что всю схему доведут до результата. Если не нравится именно [[nodiscard]], то извините, я не дизайнер языков. Я прикладник и мне нужен режим, в котором компилятор ругался бы на все необработанные мной ошибки. В этом суть моего разочарования. Включить такой режим для отдельной функции атрибутом — меня бы устроило.


            1. Janycz
              17.08.2026 07:49

              И исключения не нужны.

              Я считаю, что нужны. Ибо result.unwrap() паникует, где result это Result<T, E>, если result содержит ошибку. Лучше, чтобы в этом случае бросалось исключение. Ибо панику можно обработать только глобально, а исключение -- локально.


  1. antoshkka Автор
    17.08.2026 07:49

    <del>