Есть на ютубе видео на пятьдесят минут с гордым названием «худший язык программирования всех времён». Не удивлюсь, если вы подумаете, что оно про C++. Оно действительно про плюсы и я его смотрел где-то с полгода назад, ну как смотрел... пробежался на x2 с перемотками, мало ли что обиженный джун там наговорил, но добрый @alyokhinопять про него напомнил, и теперь я его посмотрел полностью. И знаете что самое неприятное? Если убрать интонацию обиженного джуна и оставить только аргументы, то процентов семьдесят там будет правды. Не «спорно», не «зависит от контекста», а буквально правда, которую любой разработчик, проведший с языком пару лет, подтвердит вам не задумываясь.

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

Жанр «почему C++ ужасен» на Хабре выжжен дотла и про Init-зоопарк, перегруженный static, vector названный неправильно, std::move который не move, супер медленный regex, медленный unordered_map вы всё читали раз по двадцать. Сам по себе список этих болей давно не новость, от себя добавлю, что все жалобы и примеры ниже - это следствия одного решения, и я к нему приду. Или открывайте спойлеры, там скрыта история, почему каждая часть языка получалась так, как получалась.


Много способов инициализировать переменную

Начнём с самых адов, с начала любой программы, т.е. с создания переменной. И если в других языках это обычно одна операция, то в C++ про это пишут целые книги и проводят исследования, вытаскивая потом это все на конференции, как будто других проблем мало. Помогайте. Я ничего не забыл?

int f;                    // automatic storage, мусор
static int f_static;      // но в namespace/static scope уже ноль (zero-init на старте)
std::string s;            // вызов default-конструктора
int* p = new int;         // куча, мусор
int e{};                  // 0
int e2 = {};              // 0
int* p = new int{};       // 0
int* q = new int();       // тоже 0
auto t = T{};             // временный, value-init
int b(5);
std::string s("hello");
T obj(a1, a2);
auto p = new T(a1, a2);
int x = static_cast<int>(3.5);   // каст, это тоже direct-init
int y(int(5));                   // functional notation
int a = 5;
std::string s = "hello";
T obj = other;
f(arg);                   // передача по значению, инициализируется ПАРАМЕТР функции
                          // не видимая переменная
f({1, 2, 3});             // то же
return x;                 // возврат по значению
return {1, 2, 3};         // тоже, если не сработает copy-elision
throw x;                  // инициализируется объект исключения
int c{5};                 // direct-list
int d = {5};              // copy-list
std::vector<int> v{1, 2, 3};
std::vector<int> v2 = {1, 2, 3};
std::vector<int> a(10);   // 10 элементов, все нули
std::vector<int> b{10};   // ОДИН элемент со значением 10
struct Point { int x, y; };
Point p{1, 2};            // можно так
Point p2 = {1, 2};        // а можно эдак
int arr[3] = {1, 2, 3};   // и с нулями, и без них
int arr2[3] = {};         // все нули
Point p3{.x = 1, .y = 2}; // designated initializers, C++20
Point p4{1};              // x=1, y=0 (хвост value-init)
int& r = x;
const int& cr = 5;        // привязка к временному + продление жизни
int&& rr = 10;
constexpr int n = 42;     // обязана быть constant-init
const int m = foo();      // может быть как constant, так и dynamic
constinit int g = bar();  // C++20: гарантирует static-init, но НЕ делает const
static int s = compute(); // dynamic-init, со своим Static Init Order Fiasco
struct S {
    int x = 5;            // default member initializer
    int y{10};
    S() : y{2}, x(1) {}   // member initializer list (порядок по объявлению, не по списку)
};
auto [a, b] = std::pair{1, 2};   // structured bindings, C++17
for (int v : arr) { }            // range-based for тоже инициализирует v

Если посчитать "сематнику поведения" инициализации, то их выходит штук десять (= v, (v), {v}, = {v}, {}, = {}, () в new, и т.д.), а семантических категорий будет девять, и отображение между ними не один-к-одному, и одна запись {} живёт сразу в value-init, list-init и aggregate-init в зависимости от типа справа. По теме инициализации написана отдельная книга на триста страниц, и это не шутка, мне её рекомендовал как-то один знакомый, она реально существует и реально на триста страниц, как говорится, наслаждайтесь.

И каждый метод делает чуть-чуть разное, особенно мне нравится разница между e и f, где наличие или отсутствие пары фигурных скобок определяет, лежит у вас в переменной ноль или то, что осталось в этом куске стека от прошлого вызова функции.

struct S {
    int x = 5;     // default member initializer
    int y{10};
};

NSDMI, Non-Static Data Member Initializer, инициализатор нестатического члена-данных прямо в объявлении класса. В точке int x = 5; объекта ещё не существует и ничего не инициализируется. Это default member initializer, т.е. просто заготовка, которая будет использована при конструировании, ЕСЛИ конструктор не проинициализировал член сам в mem-init-list, то есть это «инициализатор про запас», а не инициализация. Формально это brace-or-equal-initializer члена.

auto [a, b] = std::pair{1, 2};

А здесь инициализируется скрытый безымянный объект (hidden object или decomposition object , назовём его e), а a и b будут не отдельные переменные, это имена-привязки к частям безымянного объекта. Инициализация применяется к e, не к a/b. Тонкость, но раз уж придираемся, повторюсь это не совсем настоящие переменные, а поля структуры и сам e напрямую недоступен, но физически хранение примерно такое:

e
+---------+
| first   | <--- a
| second  | <--- b
+---------+

auto e = std::pair{1, 2};
auto& a = e.first;
auto& b = e.second;
Асм
int k, e;

int main() {
    auto [a, b] = std::pair{k, e};

    return a + b;
}
leaq    -12(%rbp), %rdi
leaq    k(%rip), %rsi
leaq    e(%rip), %rdx
callq   std::pair<int, int>::pair<int&, int&, true>(int&, int&)

Вот тут создаётся скрытый объект structured binding.rdi это первый аргумент конструктора, т.е. адрес места, куда конструируется pair

main:
        pushq   %rbp
        movq    %rsp, %rbp
        subq    $32, %rsp
        movl    $0, -4(%rbp)
        leaq    -12(%rbp), %rdi  <<<<<
        leaq    k(%rip), %rsi    <<<<<
        leaq    e(%rip), %rdx    <<<<<
        callq   std::pair<int, int>::pair<int&, int&, true>(int&, int&) <<<<<
        leaq    -12(%rbp), %rdi
        callq   tuple_element<0ul, std::pair<int, int>>::type&& std::get<0ul, int, int>(std::pair<int, int>&&)
        movq    %rax, -24(%rbp)
        leaq    -12(%rbp), %rdi
        callq   tuple_element<1ul, std::pair<int, int>>::type&& std::get<1ul, int, int>(std::pair<int, int>&&)
        movq    %rax, -32(%rbp)
        movq    -24(%rbp), %rax
        movl    (%rax), %eax
        movq    -32(%rbp), %rcx
        addl    (%rcx), %eax
        addq    $32, %rsp
        popq    %rbp
        retq

k:
        .long   0

e:
        .long   0

И как только вы решили, что разобрались, выясняется что фигурные скобки в одном контексте это агрегатная инициализация, а в другом это std::initializer_list, и одна и та же строчка кода может вернуть разный тип в зависимости от версии стандарта, под которую вы собираетесь. Не разный результат, а разный тип. Добро пожаловать.

auto i = 1;      // int всегда
auto j = {1};    // initializer_list<int> всегда (copy-list-init)
auto k{1};       // C++11: initializer_list<int>;  C++17+: int
Как появился зоопарк

Зоопарке инициализаций вырос именно из попытки этот зоопарк закрыть, и в 2011-м комитет искренне попытался свести всё к одному синтаксису, а в итоге добавил еще один вольер с типами.

От C достались для обычного присваивания при объявлении (int a = 5;) и фигурные скобки для агрегатов вроде массивов и структур (int arr[] = {1,2,3};). Скобки в C умели разложить значения по полям POD-агрегата, и ни на что больше не претендовали.

Потом C++ принёс конструкторы, а раз есть конструктор, то его надо как-то вызвать в точке создания объекта и самым естественным синтаксисом оказались круглые скобки, потому что вызов конструктора визуально похож на вызов функции, поэтому Widget w(args);. Логично? Логично.

Ровно до того дня, когда вы пишете Widget w(); и обнаруживаете, что объявили функцию, возвращающую Widget. Это знаменитый most vexing parse, и это не баг чьей-то реализации, а прямое следствие грамматики, унаследованной от C, где «объявление выглядит как использование», но объявление функции синтаксически неотличимо от создания объекта с пустыми скобками.

Дальше подъехал C++11 и большая идея под названием uniform initialization. Замысел был хороший, с попыткой ввести единый синтаксис, {}, который работает везде: и для агрегатов, и для классов с конструкторами, и для скаляров, и для контейнеров.

Заодно он бы чинил most vexing parse, потому что Widget w{}; нельзя распарсить как функцию, и еще запрещал сужающие преобразования вродеint x{3.5}; что было бы ошибкой, а int x = 3.5; отрезало бы дробную часть.

То есть {} задумывался не как «ещё один способ», а как тот самый, единственно правильный и рассово верный, к которому все должны перейти.

Тут включается то самое, к чему всё в этом языке сводится, о чем я написал в конце. Чтобы {} стал единственным, надо было выкинуть = и (), а на них держатся миллиарды строк кода, движки, либы, чужие API.

Получается что выкинуть нельзя, поэтому новый универсальный синтаксис не заменил старые, а поселился в свой вольер, и способов стало большею. Инструмент, придуманный, чтобы прекратить зоопарк, стал самым заметным его обитателем.

А чтобы было совсем весело, те же фигурные скобки нагрузили вторым смыслом черезstd::initializer_list, теперь если у типа есть конструктор от списка инициализации, скобки начинают означать его, причём в разрешении перегрузки он выигрывает.

Отдельная история это различие междуint e{}; (ноль) и int f; (мусор). Это осталось от дедушки C, где автоматические переменные не обнулялись. Не потому что забыли, а потому что обнуление стоит тактов, а философия C была «ты не платишь за то, чем не пользуешься» и если хочешь ноль, напиши ноль сам.

Поэтому int f; оставляет на стеке то, что осталось от прошлого вызова, и это by design, фича из 1972-го вроде года. И Страуструп позже захотел дать безопасный нулевой дефолт, но навязать его не мог, потому что это сломало бы и совместимость, и саму идею «не платить за лишнее». Так и вышло, что безопасный вариант есть, но он не по умолчанию, потому что само правило по-умолчанию зафиксировали полвека назад ради скорости.

Собрать всё обратно в один синтаксис теперь не получится, и на каждом способе висит свой код и свои правила работы с ним. Поэтому, когда по теме инициализации выходит очередная книга на триста страниц и блок-схемы на пол-экрана, то это не авторы развлекаются, это цена того, что в язык насыпали за сорок лет из-за самой главной фичи языка.

Простые вещи просто не делаются

Хрестоматийный пример сложной простоты это случайное число, и где нибудь в джаве или пайтоне вы просто напишете random.randint(1, 100) и пойдёте дальше кодить, но не здесь. Это слишком просто, чтобы быть правдой.

std::random_device rd;
std::mt19937 gen(rd());                          // а что такое mt19937?
std::uniform_int_distribution<int> dist(1, 100); // а почему отдельно?
int value = dist(gen);                           // ну наконец-то

Код не то чтобы нечитаемый, но приходтся через раз его смотреть на cppreference, когда он реально нужен. Потом я иду гуглить, что такое этот mt19937, это вихрь Мерсенна, живите теперь с этим знанием, и да, вам теперь придётся знать название генератора псевдослучайных чисел, чтобы просто кинуть кубик.

Простые вещи

До C++11 в языке жил rand(), унаследованный от C, который отдавал число в диапазоне до RAND_MAX, который стандарт гарантирует от 32767, то есть на некоторых платформах вы физически не можете получить равномерное число в большом диапазоне.

Привычное rand() % 100 даёт смещение, потому что 100 не делит нацело число возможных значений, так что одни числа выпадают чаще других, а последовательность невоспроизводимой между компиляторами. Т.е. это была простая функция, чтобы выстрелить себе в ногу.

Поэтому в C++11 затащили принципиально новый дизайн из Boost.Random, который намеренно разнёс то, что rand() свалил в кучу. Теперь отдельно сам движок генерации, или источник сырых битов (mt19937, тот самый вихрь Мерсенна Мацумото и Нисимуры из 97-го). Отдельно идет распределение, которое превращает сырые биты в равномерное-в-диапазоне без всякого смещения и отдельно идет источник сида.

Это спроектировали люди, которым нужны были воспроизводимые прогоны Монте-Карло для научных симуляций, и для их задач это работало идеально. Беда в том, что построили красивый собор, а маленькую дверь для «брось кубик от 1 до 100» так и не приделали и в стандарте до сих пор нет однострочного randint.

Но даже «правильное» заклинание тоже неправильное, потому что mt19937 хранит почти двадцать тысяч бит состояния, а мы засеиваем его одним 32-битным числом из random_device, то есть недосеваем. Это раз.

А сам random_device стандарт разрешает делать детерминированным и на старом MinGW он годами выдавал одну и ту же последовательность при каждом запуске, то есть длинная корректная мантра в реальности и длинная и коррекная, но не везде работает, хоть rand() % 100 и короче и хотя бы работает.

Проблемы кастинга

Кастинг это вообще отдельная песня и там, где в Java вы пишете значение в скобочках и всё, в C++ этих кастов целый набор, на любой вкус и цвет. static_cast, dynamic_cast, reinterpret_cast, const_cast, bit_cast, и каждый для своего случая, и каждый надо набирать руками целиком, еще есть скрытый rvalue_cast, но о нем чуть ниже.

double d = 3.9;
int i = static_cast<int>(d);        // 3, дробная часть отброшена

Base* b = new Derived;
auto* der = static_cast<Derived*>(b); // даун-каст БЕЗ проверки 
                                      // ты ручаешься, что там Derived

Base* b = get_something();
if (Derived* d = dynamic_cast<Derived*>(b)) {
    d->derived_only();              // сюда зашли, только если это реально Derived
}                                   // иначе d == nullptr

int x = 42;
std::uintptr_t addr = reinterpret_cast<std::uintptr_t>(&x);  // адрес как число

void legacy_api(char* s);           // не трогает строку, но забыл const

const std::string str = "hello";
legacy_api(const_cast<char*>(str.c_str()));   // ок, раз api реально не меняет

float f = 1.0f;
auto bits = std::bit_cast<std::uint32_t>(f);  // 0x3F800000, без UB

// как делали до C++20:
std::uint32_t old;
std::memcpy(&old, &f, sizeof f);              // то же самое, но руками

int i = (int)d;                     // компилируется, выглядит знакомо

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

// антипаттерн: «мне надоело писать static_cast»
template <class T, class U>
constexpr T sc(U&& u) { return static_cast<T>(std::forward<U>(u)); }

int i = sc<int>(3.9);               // коротко, да, правильно? нет

// или ещё хуже:
#define CAST(T, x) static_cast<T>(x)

Тут вообще засада и часто правильный C++ выглядит неправильно, потому что короткое и красивое решение почти всегда оказывается глючным и некорректным, а корректное выглядит как будто вы зачем-то усложнили жизнь свой команде. Интуиция «как должен выглядеть хороший код» нарабатывается годами, и пока она не наработана, вы живёте с ощущением, что делаете что-то не так. Спойлер: вы и правда делаете что-то не так, просто язык такой.

А чё так много?

В C есть один каст (с-style cast), (T)expr, и он делает всё. Меняет значение, переинтерпретирует указатель, снимает const, режет тип - и всё это выглядит абсолютно одинаково. А значит, вы не можете найти в кодовой базе места, где кто-то снимает константность, потому что они неотличимы от любого другого приведения.

C++ разбил этот швейцарский нож на четыре именованные операции: static_cast, dynamic_cast, const_cast, reinterpret_cast из-за этой неразличимости операций.

Чтобы намерение было явным и const_cast который «я тут подрываю const» ловился глазками на ревью.

Чтобы это грепалось и можно было найти все reinterpret_cast и проверить каждый.

Касты сами по себе зло, поэтому их сделали уродливыми специально. Как сказал Александреску, "каст это запах кода", и Страуструп делал синтаксис громоздким, чтобы приведение было больно набирать, а когда вы всё же приводите один тип к другому явно, это бросается в глаза на ревью. Старый C-каст был слишком лёгким и невесомым, и именно эта лёгкость делала его опасным.

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

Ключевые слова, которые врут

В нормальном мире ключевое слово описывает то, что оно делает, но в C++ это стало опциональной нагрузкой. Вспомните static.

А вспомнили сколько у него значений? Сделать переменную, которая живёт между вызовами функции, это раз. Сделать переменную или метод общими для всех экземпляров класса, это два. Третье значение, когда static перед функцией в .cpp файле делает её невидимой снаружи этого файла, то есть это private, но назвали static. Почему не internal, не private, не file_local? А потому что исторически так сложилось, что является ответом примерно на половину вопросов в этой статье.

void counter() {
    static int calls = 0;   // инициализируется ОДИН раз, при первом заходе
    int local = 0;          // обычная локальная, каждый вызов заново
    ++calls;
    ++local;
    std::cout << "static: " << calls << ", local: " << local << '\n';
}

И есть поведение static, которое пришло после С++11, теперь ваш staticозначает еще и light mutex guarded. Знали про это?

Logger& logger() {
    static Logger instance;   // одна строчка
    return instance;
}

Logger& logger() {
    // БЫСТРЫЙ путь: младший байт guard != 0 → уже проинициализировано
    if ((reinterpret_cast<volatile char&>(__guard_for_instance)) == 0) {

        // МЕДЛЕННЫЙ путь: сюда заходим только в первый раз и под синхронизацией
        if (__cxa_guard_acquire(&__guard_for_instance)) {
            // acquire вернул 1 → именно этот поток обязан инициализировать.
            // Остальные потоки сейчас СПЯТ внутри __cxa_guard_acquire.
            try {
                ::new (&__instance_storage) Logger();          // вызов конструктора
                __cxa_guard_release(&__guard_for_instance);    // ставим флаг, будим спящих
                __cxa_atexit(&destroy_logger, ...);            // регистрируем уничтожение
            } catch (...) {
                __cxa_guard_abort(&__guard_for_instance);      
                throw;
            }
        }
        // проигравшие гонку потоки вышли из acquire уже после release
    }
    return reinterpret_cast<Logger&>(__instance_storage);
}
Почему static делает четыре несвязанные вещи

В C у static уже было два значения. Первое, обычное и оно про время жизни, когда переменная живёт всю программу.

Второе было про внутреннее связывание, когда имя не было видно за пределы единицы трансляции. И пусть семантически это вещи из разных вселенных и одна про память, а другая про видимость, но K&R их сделали одним ключевым словом, потому что ключевых слов в языке должно быть мало, а слово static было близко под «статически размещённое».

C++ унаследовал эти значения целиком, что называется, не глядя, потому что отказаться от куска C означало отказаться от совместимости с C, а как раз этого хотели избежать. Дальше Страуструпу понадобились члены класса, общие для всех экземпляров, и можно было бы ввести что-то вродеshared, classwide, по аналогии с private, protected ,что угодно описательное, но вы понимаете каждое новое ключевое слово в языке - это мина для разработчиков.

В мире уже обязательно кто-то назвал переменную shared или internal, или classwideи в день, когда это слово становится ключевым, их код перестанет компилироваться. Поэтому прежде чем выдумывать новое слово, сначала старались переиспользовать существующее, аstatic уже жил в языке и уже был мутным, поэтому на него можно было навесить ещё мутности, без риска сломать кому-то прод. Так появилось третье значение, не потому что оно подходит по смыслу, а потому что слово было под рукой и ничего не ломало.

Четвёртое поведение подъехало в C++11, когда инициализацию функционально-локальной static захотели сделать потокобезопасной, и компилятор начал вставлять скрытый guard при первом проходе.

Язык один раз попытался это разгрести и в C++98 файловый static для внутреннего связывания объявили нежелательным и сказали пользоваться безымянными неймспейсами, но в C++11 эту рекомендацию отменили, потому что отучить людей от привычного слова оказалось сильно дороже, чем поправить язык, то есть язык не смог выкинуть даже то значение, которое сам же признал лишним.

Теперь ни одно из четырёх не снять уже не получается, и на каждом висит чей-то код, какой-то локальный счётчик, или приватная функция, или же член класса в API, который используются сто тысяч человек.

inline когда-то он просил компилятор заинлайнить функцию, но сегодня компилятор умный и инлайнит сам, когда считает нужным, а inline теперь решает проблемы линковки и One Definition Rule. И inline на функции и inline на переменной делают семантически противоположные вещи, и у функции это разрешение размножить код, а у переменной будет запрет размножить данные.

// math.h
inline int square(int x) { return x * x; }

// a.cpp
#include "math.h"
int use_a() { return square(3); }

// b.cpp
#include "math.h"
int use_b() { return square(4); }

// counter.h
inline int g_calls = 0;   // ровно один экземпляр на всю программу

// a.cpp
#include "counter.h"
void hit_a() { ++g_calls; }

// b.cpp
#include "counter.h"
void hit_b() { ++g_calls; }

// config.h
struct Config {
    static inline int instances = 0;          // один счётчик на все объекты, прямо в .h
};

inline constexpr double kPi = 3.14159265358979;  // header-only константа, один объект
Чем inline был задуман

В раннем C++ это была типобезопасная замена #define-макросу вроде «подставь тело функции прямо в точку вызова вместо call» и это была буквально оптимизация разворачивания. Отсюда имя, отсюда и народное поверье, что inline это про «сделать быстрее», забудьте, это уже лет пятнадцать как не про это.

Сегодня его несущий смысл уже не про оптимизацию, а про ODR и линковку. inline-сущность разрешено определять в нескольких единицах трансляции (то есть положить определение в заголовок и включать его всюду), и линкер обязан слить эти определения в одно, а не упасть с multiple definition.

Как inline относится к настоящему инлайнингу? Да почти никак. Как подсказка оптимизатору он чисто совещательный, и современные компиляторы его часто игнорируют, имея cost-модели, через которые они спокойно встраивают функции без inline и отказываются встраивать помеченные, а с LTO встраивают и через границы TU вне зависимости от слова.

Единственная связь inline с реальным инлайнингом сейчас косвенная, и теперь это ключевое слово позволяет положить тело в заголовок, не словив ошибку линковки, а видимое тело оптимизатор может разместить в отдельном TU, потому что без этого LTO сделать нельзя.

Методы, определённые прямо в теле класса, уже неявно помеченыinline, и всеconstexpr-функции и тоже неявно сделаныinline, но народ всё равно дописывает ключевое слово «на всякий случай», хотя оно там ничего не меняет.

Встраивать теперь надо черезforceinline (MSVC) и attribute__((always_inline)), [[gnu::always_inline]] да и те часто отказывают на рекурсии, varargs или взятии адреса. Это имя осталось рудиментом эпохи, когда оно и правда так работало, но теперь оно про линкер.

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

Чтобы понять, к чему относится const в объявлении указателя, к значению или к самому указателю, есть официальное правило «читай справа налево». Напомню, что большинство людей читают слева направо.

const int a = 5;   // "west const"
int const b = 5;   // "east const" ровно то же самое, оба константы

const int* p = &a; // указатель на const int
int const* q = &a; // и это то же самое

struct Cache {
    mutable int hits = 0;   // можно менять даже у const-объекта
    int value = 0;
};

const Cache c;
c.hits++;       // ОК, mutable
// c.value++;   // ошибка, обычный член const-объекта

struct S {
    mutable const int* p = nullptr;  // ВАЛИДНО
};

struct Bad {
    mutable const int x = 5;  // НЕВАЛИДНО, mutable нельзя на const-член
};

// Случай 1: объект РЕАЛЬНО не const, ок
int x = 5;
const int& cref = x;
const_cast<int&>(cref) = 10;   // законно, x действительно менялся, x == 10

// Случай 2: объект РЕАЛЬНО const, Undefined Behavior
const int y = 5;
const_cast<int&>(y) = 10;      // компилируется, но это UB
                               // y может остаться 5, упасть, или что угодно

int x = 0, y = 0;

const int* p1 = &x;        // p1: указатель на const int
int* const p2 = &x;        // p2: const-указатель на int
const int* const p3 = &x;  // p3: const-указатель на const int

*p1 = 5;   // ошибка: значение const
p1 = &y;   // ок: указатель можно перенаправить

*p2 = 5;   // ок: значение менять можно
p2 = &y;   // ошибка: указатель const

*p3 = 5;   // ошибка
p3 = &y;   // ошибка
Зачем const, если память не const?

Это одно из немногих ключевых слов, которое родилось в C++ и было экспортировано в C, а не наоборот. Страуструп завёл его ещё в «C с классами» (поначалу под рабочим именем readonly), а оттуда оно уехало в стандарт C89 и зажило там самостоятельной жизнью. Но слово, придуманное, чтобы навести порядок, тут же унаследовало беспорядок грамматики, в которую его вставили.

Начнём с того, что const int и int const это буквально одно и то же, иconst это просто квалификатор типа, и сидит он в той части объявления, которую грамматика называет decl-specifier-seq, т.е. последовательностью спецификаторов.

А эта последовательность была неупорядоченной задолго до появления const ровно по той же причине что иunsigned long и long unsigned означают один тип. Теперь ужеconst наступил на старые грабли, когда порядок слов и так не имел значения, и более того, "правильная» форма" это как раз int const, потому что const относится к тому, что стоит слева от него, а const int это грамматическая поблажка, потому что такое разрешали старые компиляторы.

Именно поэтому существует целое движение «east const» (его в своё время раскручивал Джон Калб, а до него про распутывание объявлений много писал Дэн Сакс). Два написания никто не проектировал, они просто выпали из грамматики, потому что так было удобно и не ломало старый код.

const int* p;   // указатель на const int, меняй указатель, не значение
int* const p;   // const-указатель на int, меняй значение, не указатель

Тут есть разница по какую сторону от звёздочки стоит const, потому что слева от звездочки он попадает в ту самую последовательность спецификаторов и квалифицирует то, на что указывают.

А справа от звездочки он уже будет частью декларатора и квалифицирует сам указатель, и объявление специально сделали похожим на выражение, которое потом будет работать с переменной. Для int p это изящно, для int (*f)(int) это уже издевательство, и const здесь усложняем грамматику, которая трудна сама по себе.

И собрать это в единое целое опять нельзя, потому что грамматику const намертво фиксирует совместимость с дедушкой C, и поменять то, как он парсится, означало бы сломать и C, и сорок лет кода на обоих языках. Так и выходит, что слово выглядит как замок на двери, а на деле это табличка «просьба не входить», повернутая буквами к двери.

Зоопарк целочисленных типов

Сколько в C++ целочисленных типов? Около пятидесяти (если считать вместе с фиксированными псевдонимами). И размер не всех из них не фиксирован, а зависит от компилятора и платформы.

bool                    // да, bool — тоже целочисленный (integral) тип

char                    // ОТДЕЛЬНЫЙ тип
signed char             // ОТДЕЛЬНЫЙ тип
unsigned char           // ОТДЕЛЬНЫЙ тип, это три разных типа, не два

char8_t                 // C++20
char16_t                // C++11
char32_t                // C++11
wchar_t

short
unsigned short
int
unsigned int
long
unsigned long
long long               // C++11
unsigned long long      // C++11

short, short int, signed short, signed short int          // 4 написания → 1 тип
int, signed, signed int                                   // 3 написания → 1 тип
long, long int, signed long, signed long int              // 4 → 1
long long, long long int, signed long long, signed long long int  // 4 → 1
unsigned, unsigned int                                    // 2 → 1

int8_t   int16_t   int32_t   int64_t        // точная ширина (опциональны)
uint8_t  uint16_t  uint32_t  uint64_t       // 8 штук

int_least8_t  ... int_least64_t             // минимальная ширина
uint_least8_t ... uint_least64_t            // 8 штук (обязательны)

int_fast8_t   ... int_fast64_t              // «быстрая» ширина
uint_fast8_t  ... uint_fast64_t             // 8 штук

intmax_t  uintmax_t                          // самый широкий
intptr_t  uintptr_t                          // под указатель (опциональны)

std::size_t        // <cstddef>
std::ptrdiff_t     // <cstddef>
std::sig_atomic_t  // <csignal>
std::wint_t        // <cwchar>
std::streamsize, std::streamoff   // <ios>

int это не «32 бита», а это «хотя бы 16 бит, но может и 32» и гарантируется только цепочка short <= int <= long <= long long. На 64-битном Linux long это 64 бита, а на 64-битном Windows long это всё ещё 32 бита, потому что обратная совместимость (запомните это словосочетание). Чтобы получить предсказуемый 64-битный тип, есть int64_t, и я искренне не понимаю, зачем там суффикс _t, мы вроде уже не в девяностых, но изменить уже не получится, об этом под спойлером.

Отдельно прекрасны символьные типы, которых семь штук, и вы рано или поздно упрётесь в вопрос, почему символ вообще бывает знаковым и беззнаковым, как число, чем char отличается от signed char и unsigned char (а это три разных типа, не два), что такое wchar_t, и в чём разница между std::string и std::wstring, и почему из-за неё у вас на ровном месте поедет кодировка, но об этом, как нибудь в другой раз.

Летопись языка

Зоопарк типов является палеонтологической летописью всех машин, на которых C когда работал, и чтобы понять, почему int не «32 бита», надо вспомнить, на чём сам C рос.

А рос он в начале семидесятых, когда железо было очень разное и тот же PDP-11 имел 16-битные слова, а Honeywell, на который C портировали одним из первых, пользовался 36-битными словами и в некоторых режимах использовались 6-битные или 9-битные символы. Исторически byte в C вообще не равен обязательно 8 битам, в стандарте CCHAR_BIT - количество бит в минимальной адресуемой единице памяти, то есть вполне легальная машина сCHAR_BIT == 9.

У CDC слова по 60 бит, что-то было в дополнительном коде, что-то в обратном, и Ритчи принял понятное тогда решение, не фиксировать размеры вообще. Т.е. int это просто «естественное машинное слово, то, с чем процессор работает быстрее всего, но не меньше 16 бит».

Язык гарантировал только минимальные диапазоны и порядок short <= int <= long <= long long, а конкретные размеры отдавал на откуп платформе и благодаря этому один и тот же исходник эффективно собирался и на 16-битном PDP-11, и на 36-битном Honeywell, и еще на чертовой дюжине машин, а нефиксированый тип int был тем самым свойством, которое позволял C работать на любом железе.

Потом пришли 32-битные машины, и int стал 32, а когда пришли 64-битные, int там и застрял на 32, потому что мир уже был завален кодом, где sizeof(int) == 4 зашито в конфигах, и расширять int означало всё это сломать и заодно навлечь на себя поломку будущих ABI.

Но Microsoft оставила long 32-битным. Почему? Большая база кода, продуктов и пользователей, что коротко называется - обратная совместимость.

Тонны кода и сам Win32 API считали long четырёхбайтовым, гоняли его наравне с int и DWORD, сериализовали на диск и в сеть как четыре байта и сломать это сочли дороже, чем смириться с расколом. То есть long означает разное на двух платформах ровно потому, что в конце девяностых две экосистемы сделали разные ставки на совместимость.

int64_t и его суффикс _t по сути, признание ошибки, что язык не смог сделать базовые типы предсказуемыми, поэтому десятилетия спустя прикрутил сбоку второй набор, уже с фиксированной шириной и отдельным заголовком (<stdint.h> в C99, в А t это и не девяностые вовсе, это семидесятые, и память о соглашение Unix про typedef-имена (size_t, time_t, wchar_t), где t зарезервирован за реализацией, чтобы стандарт мог и дальше добавлять типы, не сталкиваясь с вашими идентификаторами.

Нестандартная библиотека

Если бы я хотел максимально запутать новичка, я бы назвал вещи именно так, как они названы в STL. Самый используемый контейнер называется vector и это у нас динамический массив, но вектор в обычном понимании это величина с направлением, и сам Александр Степанов, автор STL, признавал, что имя было ошибкой.

Если вы хотели хеш-таблицу, то придется брать std::map, но это сбалансированное дерево с логарифмическим поиском. А настоящая хеш-таблица это std::unordered_map, которой, спойлер, тоже лучше не пользоваться, потому что медленно, и это не «реализация ленивая», а это зашито в сам стандарт. Гарантии, которые std::unordered_map обязан давать, не оставляют разработчику libstdc++/libc++ выбора, кроме как сделать его медленным.

std::map<std::string, int> ordered;
ordered["banana"] = 1;
ordered["apple"]  = 2;
ordered["cherry"] = 3;

for (auto& [k, v] : ordered)
    std::cout << k << ' ';       // ВСЕГДА: apple banana cherry, по возрастанию ключа

std::unordered_map<std::string, int> hashed;
hashed["banana"] = 1;
hashed["apple"]  = 2;
hashed["cherry"] = 3;

for (auto& [k, v] : hashed)
    std::cout << k << ' ';       // порядок произвольный, зависит от хеша и бакетов

std::unordered_map<std::string, int> m;
m.insert({"key", 1});
m.insert({"key", 2});            // НЕ перезаписало, а вернуло {итератор, false}
std::cout << m["key"];           // 1, а не 2

m.insert_or_assign("key", 2);    // после C++17 вот это перезапишет
std::cout << m["key"];           // 2
m["key"] = 2;                    // или просто так

Стандарт фактически требует separate chaining (раздельное хранение) с узлами и unordered_map обязан гарантировать стабильность ссылок и указателей на элементы после insert/erase (кроме удалённого), чтобы указатель на элемент оставался валидным, даже когда контейнер рехешится.

А это возможно только если каждый элемент отдельно выделен на куче (std::pair<const Key, Value> плюс указатель на следующий), а ведро (bucket) сделано как связный список таких элементов. То есть по стандарту это не «хеш-таблица в массиве», а «массив указателей на разбросанные по куче списки». Или другой прикол с оными же:

std::unordered_map<std::string, int> m;
if (m["ключ"] == 0) { }   // если ключа НЕ было, то он только что вставился
                          // с default-значением 0. Проверка "есть ли ключ" его и создала.

operator[] на отсутствующем ключе молча вставляет default-значение, поэтому проверять наличие надо через find/contains, а не через []. Мелочь, конечно, но новички наступают на эти грабли регулярно.

А еще у контейнеров есть empty(), выгляди как почистить, но это у нас вопрос «пустой ли контейнер?». По-человечески было бы is_empty(), но нет, зато естьremove(), который вообще ничего не удаляет, зато сдвигает элементы в конец и возвращает итератор, а удалять вы будете отдельно (привет, erase-remove идиома). А ещё есть std::stoi, std::stol, std::stoll, std::stof, std::stod, std::stold, и вы должны просто знать, что это. Ведь знаете?

Стандартная ли?

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

До плюсов он пробовал сделать это в Scheme, в Ada на пару с Массером, потом пришёл в C++, и шаблоны, придуманные совсем для другого, оказались достаточно мощными, чтобы выразить все его идеи. В 93–94-м все свои наработки принёс тогда еще не комитету, а группе разработки языка, и тот, то очень редкий случай, затащил её в C++98 почти целиком.

Отсюда vector, потому что в численных вычислениях Scheme «вектор» означал одномерный непрерывный массив, так что в контексте Степанова имя было логичным. Потом спохватились и хотели назвать array , но имя уже примелькалось в стандарте и проектах, поэтому переименовать опять было нельзя, опять тоже самое, что и везде в языке.

И map из той же оперы, но имя честно описывает абстракцию «отображение ключ→значение», просто люди приходят из Java и Python, где «map/dict» по умолчанию хеш, и подсознательно ждут того же. А когда настоящую хеш-таблицу наконец добавили в C++11, очевидное имя hash_map было брать нельзя, потому что его расхватали несовместимые между собой вендорные расширения от SGI, Microsoft, Dinkumware и других компания, и опять, чтобы не сломать весь этот зоопарк уже написанных своиъ hash_map, комитет взял свободное, пусть и корявое имя — unordered_map. Так что уродливое название, просто еще один шрам от выбранной дороги к обратной совместимости.

И про ...value не забудьте

А чтобы вам было совсем хорошо, вспомните, что есть lvalue, rvalue, glvalue, prvalue и xvalue. Вспомнили?

А еще есть ситуации, где std::move делает копию, и что иногда нужен не std::move, а std::move_if_noexcept. Раз уж зашла речь про перемещение, то std::move исторически имеет неправильное название и он ничего не перемещает, а просто кастует значение к rvalue-ссылке, разрешая ему привязаться к перемещающему конструктору. Надо было назвать rvalue_cast, ах блин... тогда у нас кастов прибавится.

Не все lvalue, что xvalue

В C было два понятия, lvalue и rvalue. В C++11 появилась move-семантика, и двух категорий перестало хватать, так как понадобилось различать «именованный объект, который трогать нельзя» и «временный, у которого можно отобрать значение», но это два независимых свойства и их комбинации дают пять категорий, и появляетсяxvalue (eXpiring, «истекающий») это объект с идентичностью, из которого можно забрать значение.

Чтобы помечать так xvalue объекты понадобилась отдельная семантика, назвать которую стоило rvalue_cast, но добавлять еще один каст комитет не захотел, поэтому Говард Хиннант и компания выбрали имя по намерению, и теперт в точке вызова move надо читать как «я с этим закончил, можешь забирать».

А еще в стандартной библиотеке есть второй std::move, который как раз честно двигает элементы, так что не перепутайте тот, что двигает, с тем, что не двигает. И мне кто-то даже показывал пдф-ку на 70 страниц, как и когда правильно пользоваться обоими std::move, жаль я название не запомнил.

И это я ещё не докапываюсь до названий идиом. Помните RAII из статьи про С++101? Расшифровывается как Resource Acquisition Is Initialization, т.е. захват ресурса есть его инициализация, но описывает оно ровно противоположный момент, и в идиоме главное не захват ресурса, а его автоматическое освобождение в деструкторе при выходе из области видимости. Т.е. это будет CADR (Constructor Acquires, Destructor Releases, «конструктор захватывает, деструктор освобождает»), а не RAII (создает ресурс в конструкторе).

void process() {
    FileHandle file("data.txt");   // открыли

    if (nothing_to_do())
        return;                    // ранний выход — файл закрыт автоматически

    might_throw();                 // кинуло исключение — файл ВСЁ РАВНО закрыт
}                                  // обычный конец scope — деструктор закрыл файл

Или другой пример, CRTP - вы вчитывались в название идимы? CRTP это Curiously Recurring Template Pattern, «любопытно повторяющийся шаблон», и название описывает не то, что паттерн делает, а то, что человек, который его придумал, несколько раз на него натыкался в проектах и удивлялся этому совпадению, а потом так назвал паттерн.

Современный C++

Слышали, что надо учить современный С++? Но когда вы гуглите, что такое современный С++, вы попадаете на книги надцатилетней давности.

Effective Modern C++ Скотт Майерс (2014), и прямо в подзаголовке «42 способа улучшить использование C++11 и C++14»

Modern C++ Design: Generic Programming and Design Patterns Applied

The Modern C++ Challenge

Modern C++: Efficient and Scalable Application Development

C++11 был первым «современным», он принёс умные указатели, лямбды и move-семантику. Потом появился 14, 17, 20, 23, и каждый объявлял себя самым новым и уж теперь точно «современным». И вот уже самый продаваемый учебник по современному С++ «Effective Modern C++» уже больше не modern, а местами даже не особо effective. А индустрия в это время живёт где-то на C++17, потому что переписывать миллионы строк рабочего кода под «модерновее-модернового» никто не желает, ибо страшно.

Итог такой, что вы вынуждены знать все версии стандарта сразу (а они отличаются, уж поверьте), потому что старый код на работе написан на одном диалекте, новый на другом, в учебнике третий, в туториале на ютубе четвёртый и все они не modern.

Ошибки, которые не влезают в монитор

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

Ошибка инстанцирования шаблона это китайский нетрадиционный из угловых скобок, который занимает столько горизонтального места, что оно уже не влезает на 4K-монитор. Я серьёзно начал понимать смысл сверхшироких мониторов именно глядя на эти ошибки, а еще со временем стал отключать перенос строк, потому что с переносом становилось всё только хуже. Неудивительно, что у многих C++ разработчиков ultrawide моники, очки и больная шея.

Почему ошибки такие многословные

Шаблон это не код. Когда вы подставляете в него конкретные типы, компилятор инстанцирует его, вставляя ваши типы внутрь и только потом проверяет получившееся на ошибки. Инстанцирует весь шаблон, везде подставив ваш тип.

Теперь, когда проверка идёт в глубине алгоритма (и ваш тип не подходит для sort), то код уже разверунт на десять слоев вниз и выбраться наверх можно только вывалив это все вам на экран. По сути это утиная типизация, только перенесённая на этап компиляции. Щаблон просто работает, если операции, которые он внутри себя использует, оказались валидны. Но и режим вывода ошибок у утиной типизации тоже один, она срабатывает не там, где вы ошиблись, а где-то глубоко внутри чужой библиотеки, в точке использования, и шаблон вообще без понятия, что он вызывает в этом месте, потому что самого кода нет, есть только проверка подходит-не подходит.

Тридцать лет шаблоны не имели механизма выразить требования к параметру. Ну нельзя было написать «этому шаблону нужен сравнимый тип», потому что при утиной типизации требования всегда неявные.

Компилятор физически не мог сказать «вы нарушили требование X», потому что никакого X нигде не записано и мог только дотащить вас за шиворот на строку 4212 в недрах <algorithm> и показать, что там для вашего типа не определён operator<, а чтобы вы поверили, то приходится выложить весь стек инстанцирования по дороге.

Концепты, та самая возможность назвать требования, были идеей ещё со степановских неформальных «concept'ов конца восьмидесятых, их готовили в C++0x — и вырезали из C++11, сочтя дизайн слишком сложным (я об этом писал в одной из статей). Но долетели они только к C++20, они поэтому и называются requres (требования)

Zero-maybe abstractions

C++ последние лет двадцать продаёт себя как язык абстракций с нулевой стоимостью, но любые абстрации уже небесплатны, и std::unique_ptr медленнее сырого указателя: у него нетривиальный деструктор, а такой тип по Itanium ABI нельзя передать в регистре. Только через стек, тогда как голый указатель улетел бы в регистр.

И это не только про умные указатели, а про любой тип с нетривиальным деструктором: shared_ptr, string, vector муваются по значению через память по той же причине. Move-семантика не бесплатна, проблема в деструкторе, когда используется неразрушающее перемещение, поэтому у объекта, из которого вы переместили, всё равно отрабатывает деструктор, и эквивалентный ручной код был бы быстрее.

Регулярки в стандартной библиотеке считаются одной из худших реализаций в природе, местами в десятки и сотни раз медленнее альтернатив, а одно только подключение <regex> добавляет каждой единице трансляции хорошо если секунду компиляции.

unordered_map медленный, потому что медленный и недружелюбный к кешу, и если вам нужна скорость, вы тащите flat_hash_map из гугловского Abseil или F14 из фейсбучного Folly. Заметили закономерность? Куча вещей в C++ могла бы стать значительно быстрее, но не станет. Потому что это сломало бы ABI-совместимость, и вот мы подошли к главному.

Совместимость ценой всего

Все претензии выше, от кривых имён до медленных контейнеров, от зоопарка static до невидимых копий, сходятся в одну точку. Люди думают, что C++ это язык, который ставит во главу угла производительность, но на самом деле он ставит во главу угла обратную совместимость. Пофиг на производительность, пофиг на эргономику разработки, пофиг на опыт разработчика, пофиг вообще на всё.

Именно приверженность совместимости превратила язык в монстра, когда нельзя переименовать vector, потому что сломается миллиард строк кода, и нельзя ускорить unordered_map, потому что изменится ABI, и абсолютно точно нельзя сделать разрушающее перемещение, потому что момент упущен лет десять назад, а ломать существующее нельзя. Каждое уродство языка это каменный цветок в одном месте неправильного решения из прошлого, который нельзя выкинуть ибо застрял, потому что на нём уже что-то стоит: чья-то либа, движок, игра или пайплайн.

Но ровно то свойство, за которое C++ ругают, это то самое свойство, благодаря которому на нём написана половина мира. Совместимость это и болезнь, и причина выживания, потому что код, который вы написали двадцать лет назад, с некоторыми танцами и ударом в бубен, всё ещё будет собираться. Библиотека, которую забросили в 2008, всё ещё линкуется и для игровой индустрии, где стоимость переписывания измеряется человеко-десятилетиями, это не баг, это несущая стена.

А как же Rust?

Меня тут просили в коментариях высказаться про Rust. Надеюсь, что обойдемся без религиозных войн, он просто другой.

Rust по дизайну на порядок лучше. Стандартный компилятор, стандартная сборка, стандартный пакетный менеджер, никаких заголовочных файлов, лучшие что я видел сообщения об ошибках, нормальные значения по умолчанию, отсутствие неявных преобразований, нормальный UTF-8, sum-типы, и безопасность памяти на этапе компиляции. Многие проблемы, которые в C++ не решены и врядли будут решены в ближайшие десять лет, в Rust уже решены вчера.

Но «лучше по дизайну» не равно «надо брать». Разработка игр это быстрая итерация, творческий бардак и «давай попробуем вот так», а Rust этого не дает, и если честно - этого уже и С++ не дает, какой бы новый и современный он ни был. Поэтому игры перешли на скрипты, DSL и декларативное программирование.

А Rust по своей сути про корректность и дисциплину, и этот конфликт фундаментальный, приведший к тому, что есть растущая куча игр, которые начинали на Rust'е и ушли с него. Плюс экосистема, как бы вы не крутились, а CUDA, движки, тулинг, дофигилиарды строк готового кода, всё это C++, и будет им ещё очень долго.

[Х/Л]удший язык программирования всех времён?

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

Чтобы писать на нём правильно, нужна энциклопедическая память на исключения, а когда выучили исключения, то отдельно на исключения из исключений. Но C++ живее всех живых, и разменяет еще не один десяток, потому что у него чудовищная инерция и он один из немногих, кто выбрал совместимость любой ценой, вероятно, ценой всего. И этот выбор сделал его одновременно невыносимым и незаменимым. Полмира работает на нём, и будет работать еще долго, нравится мне это или нет.

Я пишу на этом ужасном языке, романтизируя его сложность и доказывая самому себе, что я умный. И продолжу писать на нём, потому что в моей области он пока единственный, кто выдаёт нужную производительность, став несущей стеной. А заменить эту несущую стену в "жилом" и "живом" доме это не просто «переписать пару комнат на модном языке», это снести, нафиг, весь дом и на его месте строить новый, оставшись жить пару лет на улице в будочке.

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

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


  1. MaxAkaAltmer
    01.07.2026 19:50

    Virtual static жалко нету.


  1. gevals
    01.07.2026 19:50

    Вариант: его сделали таким ужасным, чтобы только избранные могли на нем писать, защищаясь например от вайб кодеров на нем:)

    Предвидели видимо


  1. nickolaym
    01.07.2026 19:50

    Про то, что inline для функции и для переменной противоположны - неправда.

    И там и там в объектных файлах создаются объекты (в секции кода и в секции данных, соответственно), помеченные для линкера как "оставь единственный экземпляр". У MSVC для этого был атрибут __declspec(selectany).

    При этом все указатели на инлайн-функцию и инлайн-переменную принимают одинаковое значение. Это важно, например, для параметров шаблонов.

    И все статические переменные внутри тела функции также "оставь единственный экземпляр".

    Собственно, это всё меры для реализации ODR.

    Однако, у компилятора никто не отобрал право инлайнить тело функции (но не размножать её статические переменные). Но он это может сделать и без ключевого слова, кстати.


  1. slonopotamus
    01.07.2026 19:50

    и в день, когда это слово становится ключевым, их код перестанет компилироваться

    Вообще, если подумать при проектировании грамматики языка, то ключевые слова могут не конфликтовать с именами идентификаторов. Например, в Bash вполне валидная конструкция export export=export. Она присваивает переменной окружения по имени export значение export и все живы.


  1. Daddy_Cool
    01.07.2026 19:50

    Я так понимаю, ++ нынче язык который невозможно выучить весь. Интересно, насколько у разных людей оказываются несовместимые знания областей языка...


  1. eastig
    01.07.2026 19:50

    Как разработчик компиляторов я пишу на разных диалектах C++:

    Когда меня приглашают на собеседования как знатока C++, я обычно отказываюсь. Надоело объяснять, что мой С++ особенный.

    В OpenJDK Hotspot C++ ограничен по максимуму, отладка и стабильность во главе всего (попробуйте отладить JVM где JIT генерирует и перегенерирует код и куча потоков):

    Features from the C++98/03 language may be used unless explicitly forbidden here. Features from C++11, C++14, and C++17 may be explicitly permitted or explicitly forbidden, and discussed accordingly here. There is a third category, undecided features, about which HotSpot developers have not yet reached a consensus, or perhaps have not discussed at all. Use of these features is also forbidden. … Historically, HotSpot has mostly avoided use of the Standard Library.

    (It used to be impossible to use most of it in shared code, because the build configuration for Solaris with Solaris Studio made all but a couple of pieces inaccessible. Support for header-only parts was added in mid-2017. Support for Solaris was removed in 2020.)

    LLVM более демократичный:

    Unless otherwise documented, LLVM subprojects are written using standard C++17 code and avoid unnecessary vendor-specific extensions. … Instead of implementing custom data structures, we encourage the use of C++ standard library facilities or LLVM support libraries whenever they are available for a particular task. LLVM and related projects emphasize and rely on the standard library facilities and the LLVM support libraries as much as possible. … When both C++ and the LLVM support libraries provide similar functionality, and there isn’t a specific reason to favor the C++ implementation, it is generally preferable to use the LLVM library. For example, llvm::DenseMap should almost always be used instead of std::map or std::unordered_map, and llvm::SmallVector should usually be used instead of std::vector.

    We explicitly avoid some standard facilities, like the I/O streams, and instead use LLVM’s streams library.


  1. vanxant
    01.07.2026 19:50

    Этот список ходит по инторнетам лет 25, примерно со второй книги Майерса.

    Где-то по дороге потеряли 15 разных значений f(x).

    За это время вышло штук 7 мажорных апдейтов языка, но в целом стало только хуже, потому что deprecated не завезли, и а вдруг вы захотите скомпилить код из 1969 года.


  1. Gapon65
    01.07.2026 19:50

    Презабавная статья!

    Автор доказал, что C++ это, на самом деле, лучший язык программирования :) В условиях естественного отбора, при наличии разнообразных ограничений, выживают не самые сильные, красивые (и далее по списку), а те, кто способен к адаптации. В отношении C++ можно сказать, что это язык который поддерживает множество парадигм программирования:

    • процедурное

    • объектно-ориентированное

    • шаблонное (generic programmimg)

    • функциональное

    • параллельное (и, что очень важно, с четко определенной семантикой модели памяти)

    Язык предлагает несколько уровней абстракций. Не буду их анализировать здесь. Это отдельная тема.

    Производительность языка максимально близка к железу.

    Существует огромное количество библиотек и фреймворков.

    Сотни тысяч (миллионы) программистов знают (хотя и в разной степени) этот язык.

    Язык хорошо стандартизирован (есть комитет по стандартизации, в котором представлены интересы больших корпораций) и документирован.

    В совокупности, все эти факторы позволяет использовать язык для разработки практически любых классов приложений: моделирование, обработка данных, системы управления, системы реального времени, встроенные приложения (микроконтроллеры, GPU), системы высокочастотной торговли, сервисы, сетевые приложения, базы данных, игры, 99% модулей для Пайтона, и т.д. и т.п.

    Ни один другой язык и близко не обладает всей совокупностью качеств C++. Вопрос лишь в том, каким образом его используют. В этом плане, C++ мало чем отличается от естественных языков на котором говорят люди. Например, на (условно) английском в мире говорят сотни миллионов людей. В этом языке 2.5 миллиона слов. Однако речь забулдыги из подворотни, жителя гетто, продавца автомобилей и представителя высшего сословия отличаются так, как если бы они говорили на совершенно разных языках. Та же самая картина и у программистов на C++.

    Ну и еще один комментарий. Автор статьи явно не понимает почему генерация случайных чисел реализована именно так (хотя вопрос не возник бы в принципе, если бы автор прошел хотя бы вводный курс математической статистики):

    std::random_device rd;
    std::mt19937 gen(rd());                          // а что такое mt19937?
    std::uniform_int_distribution<int> dist(1, 100); // а почему отдельно?
    int value = dist(gen);                           // ну наконец-то

    Ответ здесь очень простой. Когда речь идет о генерации случайных чисел, то есть две вещи:

    • функция распределения (uniform, Gauss, etc.). Строка 3 вверху.

    • собственно генератор случайных (или псевдо-случайных) последовательностей. Строка 2 вверху.

    Библиотека четко разделяет эти концепции, предлагая программисту максимальную свободу выбора генератора в соответствии с требованиями задачи. Например:

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

    • в ряде случаев (серьезная криптография) требуется более надежный генератор, либо генератор на основе физических эффектов (квантовых) с привлечением специализированного железа. Такие генераторы будут генерировать действительно случайные и невоспроизводимые последовательности.

    • либо может потребоваться очень быстрый генератор (заметьте, что mt19937 является довольно медленным). И тогда программист может разработать свою версию которая удовлетворяет стандартному интерфейсу.

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

    Ну и если уж речь идет о том, что Вам непонятно что такое mt19937то напишите простой класс и используйте его в своем коде так, как если бы Вы писали на Пайтоне или Ржавчине :)

    claсс RandomUniformInt {
    public:
        MyRandomInt(int minVal, maxVal) : _gen(_rd()), _dist(minVal, maxVal) {}
        int next() { return _dist(_gen);}
    private:
        std::random_device _rd;
        std::mt19937 _gen;
        std::uniform_int_distribution<int> _dist;
    };
    
    // Client code
    
    RandomUniformInt randInt(1, 100);
    int val = randInt.next();
    


  1. yurrig
    01.07.2026 19:50

    Читаю и недоумеваю. Десятилетиями пишу на C++, с 98 по 23, никаких особых проблем не было никогда. У меня какой-то другой C++?