
Есть на ютубе видео на пятьдесят минут с хвостиком с гордым названием «худший язык программирования всех времён». Не удивлюсь, если вы подумаете, что оно про 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>, _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++
Слышали, что надо учить современный С++? Но когда вы гуглите, что такое современный С++, вы попадаете на книги ХХ-надцатилетней давности.
Modern C++ Design: Generic Programming and Design Patterns Applied
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, они поэтому и называются requires (требования)
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, и продолжайте писать на худшем языке программирования.
Комментарии (279)

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

Rive
01.07.2026 19:50Эзотерические языки как способ застолбить за собой рабочее место в шутку предлагали на Хабре ещё лет десять назад.

UserSergeyB
01.07.2026 19:50Вайбкодерам не нужен язык программирования, ни простой, ни сложный.

gevals
01.07.2026 19:50Иногда хотя бы основы языка надо знать, чтобы где то что то понять и т.д, совсем то нулевой вайбкодер, который в детстве даже Бейсик и паскаль не изучал, кажется вообще толком ничего не сделает..

nickolaym
01.07.2026 19:50Про то, что inline для функции и для переменной противоположны - неправда.
И там и там в объектных файлах создаются объекты (в секции кода и в секции данных, соответственно), помеченные для линкера как "оставь единственный экземпляр". У MSVC для этого был атрибут
__declspec(selectany).При этом все указатели на инлайн-функцию и инлайн-переменную принимают одинаковое значение. Это важно, например, для параметров шаблонов.
И все статические переменные внутри тела функции также "оставь единственный экземпляр".
Собственно, это всё меры для реализации ODR.
Однако, у компилятора никто не отобрал право инлайнить тело функции (но не размножать её статические переменные). Но он это может сделать и без ключевого слова, кстати.

slonopotamus
01.07.2026 19:50и в день, когда это слово становится ключевым, их код перестанет компилироваться
Вообще, если подумать при проектировании грамматики языка, то ключевые слова могут не конфликтовать с именами идентификаторов. Например, в Bash вполне валидная конструкция
export export=export. Она присваивает переменной окружения по имениexportзначениеexportи все живы.
NeoCode2
01.07.2026 19:50export export=export
Вот такого точно не надо, код станет еще запутаннее. И bash это тоже антипример.
А в том чтобы программа перестала компилироваться после введения в новую версию языка новых ключевых слов, я не вижу ничего плохого. Зачем тогда нужны программисты? Исправят, тем более компилятор укажет все такие места.

Sixshaman
01.07.2026 19:50Хуже если она не перестанет компилироваться, а изменится семантика у части кода. Тогда о проблемах станет известно далеко не сразу.

R0bur
01.07.2026 19:50Реализация в PHP прекрасна: идентификатор переменной должен начинаться с $. Поэтому переменные в любых контекстах всегда различимы.

event1
01.07.2026 19:50Вы, таки будете смеяться, но в крестах тоже можно начинать переменные с $.

dalerank Автор
01.07.2026 19:50но это везде порицаемо, спецсимволы не должны пролезать в имена, с $ видимо недоработка вышла

arteast
01.07.2026 19:50Нельзя так-то, но компиляторы разрешают как расширение. Есть https://wg21.link/P4234 предложение, чтобы стало можно там, где это можно.

Astrowalk
01.07.2026 19:50Этак можно докатиться до венгерской нотации (https://ru.wikipedia.org/wiki/Венгерская_нотация )

Kelbon
01.07.2026 19:50чтобы это сделать нужно сделать лексер зависимым от контекста. Т.е. по разному разбирать символы на лексемы в зависимости от того какую конструкцию мы парсим. А это плохо и никому не нужно

rsashka
01.07.2026 19:50А вы уверены, что тут нужно править именно лексер? Мне кажется, что подобная логика должна анализироваться даже не в парсере (так как с точки зрения синтаксиса тут все корректно), а на еще на уровень выше, в анализаторе.

ZirakZigil
01.07.2026 19:50Так лексер и стоит на уровень выше, перед парсером. И, собственно, в этом и проблема, что если имеется зависимость от контекста, то в лексер придётся тащить те вещи, которые в противном случае живут только на уровнях парсера и ниже.

rsashka
01.07.2026 19:50Ниже-выше зависит от направления взгляда.
“Выше” я писал про уровень абстракции, тогда как лексер, это самый низкий уровень разбора входных данных, который реализуется с помощью конечных автоматов. Поэтому там в принципе не может быть зависимости от контекста, в отличии от более высоких по уровню абстракции уровнях.

ZirakZigil
01.07.2026 19:50поэтому там в принципе не может быть зависимости от контекста
В контекстно-независимом языке из "export export=export" лексер выплёвывает что-то типа {keyword, space, keyword, sign_eq, keyword}, потом парсер обламывает.
В контекстно-зависимом языке [где можно так писать] лексер должен выплюнуть {keyword, space, identifier, sign_eq, identifier} чтобы парсер не обломал. Вот для того чтобы понимать почему "export" то keyword, то identifier и требуется понимание контекста.
Вы, конечно, можете возразить, мол, пусть гонит первый вариант, а уже парсер пусть разбирает, что вот тут keyword это keyword, а вон там — identifier. Но тогда получается, что мы на парсер перекладываем работу лексера [по определению типа токена]. Жабогадюкинг.

rsashka
01.07.2026 19:50Но тогда получается, что мы на парсер перекладываем работу лексера [по определению типа токена].
Так это и есть контекстно-зависиый синтаксис, о котором лексер на его конечных автоматах ничего знать не должен :-)

ZirakZigil
01.07.2026 19:50Для меня ситуации "лексер знает о работе уровня парсера" и "парсер знает о работе уровня лексера" одинаково "ниочень".

mayorovp
01.07.2026 19:50Даже если запретить контекстные ключевые слова - в современных языках лексеру всё равно есть где споткнуться. Например, на вложенной интерполяции строк.

Yuuri
01.07.2026 19:50export export=export— это вполне себе контекстно-свободная конструкция, а парсер может быть реализован без лексера (т. н. scannerless parser), его нет в требованиях грамматики.

nickolaym
01.07.2026 19:50Нифига. У C и С++ синтаксис контекстно-зависимый, к сожалению.
Типы, функции, переменные. Плюс шаблоны. Там синтаксическое дерево существенно разное, поэтому парсер должен знать, что стоит за тем или иным словом.

rsashka
01.07.2026 19:50чтобы это сделать нужно сделать лексер зависимым от контекста. Т.е. по разному разбирать символы на лексемы в зависимости от того какую конструкцию мы парсим. А это плохо и никому не нужно
Парсер - да, но не лексер.

nickolaym
01.07.2026 19:50Парсер - да, а не только анализатор.
А вы уверены, что тут нужно править именно лексер? Мне кажется, что подобная логика должна анализироваться даже не в парсере (так как с точки зрения синтаксиса тут все корректно), а на еще на уровень выше, в анализаторе.

nickolaym
01.07.2026 19:50Внезапно, - но лексеру пофиг.
Лексер гонит поток лексем "закорючка такая, закорючка этакая, слово латиницей".
А вот уже парсер смотрит, является ли это слово служебным или рядовым. Или как следует трактовать ту или иную закорючку. >> это один сдвиг или две угловые скобки? А!

Kelbon
01.07.2026 19:50https://github.com/llvm/llvm-project/blob/main/clang/lib/Lex/Lexer.cpp
https://github.com/llvm/llvm-project/blob/main/clang/include/clang/Basic/TokenKinds.def#L372
лексер маппит напрямую "class" -> kw_class, превращая текст в поток токенов, так что
> парсер смотрит, является ли это слово служебным или рядовым
Не соответствует коду

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

eastig
01.07.2026 19:50Как разработчик компиляторов я пишу на разных диалектах C++:
в проекте LLVM - это https://llvm.org/docs/CodingStandards.html
в проекте OpenJDK Hotspot - это https://github.com/openjdk/jdk/blob/master/doc/hotspot-style.md
Когда меня приглашают на собеседования как знатока 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.

Kelbon
01.07.2026 19:50Это не разные диалекты, это с одной стороны LLVM - "поддерживаем С++17, озаботьтесь что ваш код компилируется на С++17 и не зависит от каких-то стрёмных расширений"
А с другой стороны какая-то идиотия джавы, которая фичи делит не по логике, а по хз чему
> Когда меня приглашают на собеседования как знатока C++, я обычно отказываюсь. Надоело объяснять, что мой С++ особенный.
моё личное мнение (как контрибьютора LLVM) - все знающие хорошо С++ знают его одинаково, с одинаковым набором фич. "диалектами" можно назвать QT и unreal engine, где есть свои кастомные расширения, но и там основа есть та же. И есть пара стилейС-стайл - неоправданно близко к С
"слишком модерн С++" - шаблоны ради ничего, переусложнения, накрученные ranges
"поганая джава" - бесконечные интерфейсы и менеджеры
Все эти "стили" это разновидности плохо понятого С++, любой кто всё же осознал язык в итоге приходит к чему-то среднему и одному и тому же

nickolaym
01.07.2026 19:50На любом языке можно писать на фортране. На любом тьюринг-полном языке можно писать на хаскелле. И любая достаточно сложная программа содержит неявную кривущую реализацию лиспа.
Те, кто осознал C++, могут писать и на фортране, и на хаскелле, но стараются щадить своих коллег.
А лисп всё равно самозародится, как мыши в сене. Это неизбежно, смиримся.

ZirakZigil
01.07.2026 19:50На любом тьюринг-полном языке можно писать на хаскелле.
foo :: Eq a => a -> a -> BoolВот тут я чётко вижу, что функция позволяет делать x == y и x /= y. Я чётко вижу, что функция НЕ позволяет сделать, например, rm -rf.
template<typename T> bool foo(T x, T y);Вот тут я чётко вижу, что функция позволяет сделать что угодно, что в принципе возможно сделать из C++.
Покажите, пожалуйста, как на C++ можно писать на Хаскелле хотя бы в той мере, чтобы foo из C++ стала такой же как foo из Хаскелля.

nickolaym
01.07.2026 19:50Для начала, нам нужен тайпкласс. Если мы не будем упарываться тем фактом, что в хаскелле тайпкласс неявно создаёт рантаймовую сущность "словарь виртуальных функций", - то аналог тайпкласса - это концепт.
#include <concepts> // или руками напишем template<class T> concept ad_hoc_eq = requires (T& x, T& y) { (bool)(x == y); }; template<std::equality_comparable T> auto foo(T, T) -> bool; // вариант с GADT // (когда мы не требуем, чтобы оба аргумента были одного типа) // (и вообще не задумываемся про их тип) auto foo( std::equality_comparable auto, std::equality_comparable auto ) -> bool;
kmatveev
01.07.2026 19:50То, какой инстанс тайпкласса будет использоваться в каждом конкретном вызове, известно на этапе компиляции. Передача словаря - это деталь реализации, вполне можно было бы вместо словаря генерировать мономорфные реализации функции для каждого инстанса, наподобие того, как в C++ генерируются экземпляры шаблонных функций

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

ZirakZigil
01.07.2026 19:50
nickolaym
01.07.2026 19:50В смысле что мешает.
А что мешает в хаскелле в IO-функции писать факью одновременно с выполнением какой-то полезной работы? А что мешает в чистой функции сделать unsafePerformIO и написать факью оттуда?

arteast
01.07.2026 19:50Я думаю, это не то, что хотел увидеть вопрошающий. foo по прежнему может дернуть любой публичный член своих аргументов (звучит как-то... nsfw). Или может дернуть `system("rm -rf /");`. Если про дерганье членов я отписал рядышком, то вот увидеть по определению функции, что она точно не дергает ничего типа system в C++ действительно нельзя.

arteast
01.07.2026 19:50template<typename T> struct Eq { bool operator==(const T& other) = 0; bool operator!=(const T& other) {return !(*this == other);} }; template<typename T> bool foo(const Eq<T>& x, const Eq<T>& y);Как-то примерно так.

ZirakZigil
01.07.2026 19:50Что мешает мне внутри foo вызывать, скажем, print("fuck you")? Прочитайте ещё раз первый абзац прошлого комментария.

arteast
01.07.2026 19:50Ничто. Pure функции в C++ не завезли, увы. Впрочем, это не проблема C++, и он тут не одинок. Впрочем, вы не критиковали C++ per se, а отвечали на комментарий "на любом языке можно писать на хаскеле". Впрочем, этот комментарий имел в виду, что на нем можно писать в функциональном стиле, а не что можно из C++ сделать хаскель со всеми его гарантиями. Поэтому в foo не будет print не потому, что С++ это запретит, а потому, что вы его туда не напишете.

ZirakZigil
01.07.2026 19:50Поэтому в foo не будет print не потому, что С++ это запретит, а потому, что вы его туда не напишете.
Всё было бы хорошо, если не было плохо. Я-то сам, конечно, не буду себе в ноги стрелять (насколько могу, т.е.), но вот когда такая функция торчит из либы, то с гарантиями как-то интереснее.

ReadOnlySadUser
01.07.2026 19:50Любая consteval функция - в принципе Pure. Программируйтся всё constexpr/consteval/constinit и будет вам Pure.
Ну... почти. Есть способы даже это обойти через дыры в ADL, причем через баги в стандарте языка, а не в компиляторе) Но это уже детали)
Всё что не constexpr - это просто функции с эффектами, но такие и в Haskell могут делать что угодно)
rsashka
01.07.2026 19:50Любая consteval функция - в принципе Pure.
НЕТ. Результат вычисления чистой функция зависит только от входных параметров, тогда как consteval может использовать внешние данные. Лишь бы они были были вычисляемы на момент компиляции.

ReadOnlySadUser
01.07.2026 19:50Так, стоп. Можно открыть википедию, но в целом, смысл термина "чистая функция" сводится к тому, что при одинаковых аргументах мы получаем одинаковый результат. Доп условие в том, что мы не "меняем внешний мир", но в consteval функции это сделать нельзя.
Про внешние данные ничего не говорится. Я могу использовать сколько угодно "вшешних" данных, до тех пор пока эти данные константны. А они константны, т.к. на этапе компиляции состояние хранить нельзя (ну, можно, но через ОЧЕНЬ грязные хаки с ADL и отложенной инициализацией шаблонов, о которых я рассказывать не буду).
Соответственно сколько consteval функцию не вызывай с одинаковыми аргументами, она всегда вернет одинаковое значение. А следовательно она чистая.UPD:
Уточнение: "чистая в рамках одной единицы трансляции".UPD2:
Ладно, убедили, через макросы можно сломать чистоту :) Ну штош, по ходу и правда нет чистых функций в С++.
rsashka
01.07.2026 19:50Соответственно сколько consteval функцию не вызывай с одинаковыми аргументами, она всегда вернет одинаковое значение. А следовательно она чистая.
Тут ключевое не “константы”, а “при одинаковых аргументах мы получаем одинаковый результат”, что далеко не одно и тоже. Поэтому что внешние данные, это НЕ аргументы функции и не важно изменяются они во время компиляции или нет.
Важно то, что значение функции становится зависимым не от аргументов, а он некоторых внешних обстоятельств, который вы можете зафиксировать только на время компиляции. Но при повтором запуске компилятора у вас значению подобной функции может быть другим. Поэтому и называть её чистой (математический чистой) нельзя, так как её значение становится зависимым не только от аргументов.
consteval std::string_view compile_time() { return __TIME__; }

ReadOnlySadUser
01.07.2026 19:50Почему-то внешние данные - это не аргументы?
В хаскелле например все функции чисты, включая лямбды. А они контекст захватывают.
Можно все consteval функции рассматривать как замыкания над внешним миром
Про макросы я сноску сделал отдельную, но если не использовать их внутри consteval функции, то она чистая.

rsashka
01.07.2026 19:50UPD: Уточнение: “чистая в рамках одной единицы трансляции”.
UPD2: Ладно, убедили, через макросы можно сломать чистоту :) Ну штош, по ходу и правда нет чистых функций в С++.
На самом деле есть :-)
Просто чистые функции в С++ определятся пользователем с помощью атрибутов
__attribute__((const))или__attribute__((pure)), и они не гарантируются (не проверяется) компилятором и он полностью полагается на мнение программиста.

nickolaym
01.07.2026 19:50А если всё-таки упороться, - то... ну, придётся затащить словари. Только это уже будет явно.
using Object = std::shared_ptr<void>; // раз уж начали городить рантайм struct IEqTraits { virtual bool op_eq(Object, Object) const = 0; }; template<class T> struct EqTraits : IEqTraits { bool op_eq(Object a, Object b) const override { auto ta = std::static_pointer_cast<T>(a); auto tb = std::static_pointer_cast<T>(b); return *ta == *tb; } }; bool foo( // словари идут впереди аргументов IEqTraits const* eq, // упороться каррингом - это отдельное развлечение (не сейчас) Object a, Object b) { return eq->op_eq(a, b) && eq->op_eq(b, a); }

eastig
01.07.2026 19:50А с другой стороны какая-то идиотия джавы, которая фичи делит не по логике, а по хз чему
Как раз никокой идиотии нет. Исходникам JVM Hotspot порядка 25 лет. Там полно легаси кода. Просто так код никто не трогает. Если бы старались использовать все новые фичи C++, то было бы такое наслоение стилей. Никому не хочется заниматься отладкой C++. Без этого куча проблем с отладкой, особенно GC кода. Разработчики хотят работать на решением задач JVM, а не C++.
моё личное мнение (как контрибьютора LLVM) - все знающие хорошо С++ знают его одинаково
Перефразирую Сократа :) “я знаю, что ничего не знаю в C++” Мое знание С++ ограничено тем, что я использую часто. То что, не используется, очень быстро забывается.

vanxant
01.07.2026 19:50Этот список ходит по инторнетам лет 25, примерно со второй книги Майерса.
Где-то по дороге потеряли 15 разных значений f(x).
За это время вышло штук 7 мажорных апдейтов языка, но в целом стало только хуже, потому что deprecated не завезли, и а вдруг вы захотите скомпилить код из 1969 года.

trinxery
01.07.2026 19:50deprecated не завезли


vanxant
01.07.2026 19:50Если auto_ptr стал deprecated в С++11, то должен был быть полностью выпилен в 14 или максимум в 17. Иначе это просто ни к чему не обязывающие пожелания каких-то потусторонних дядей.
Забавно, что даже в С выпиливание устаревших фич есть (напр., триграммы, синтаксис K&R). Но не в плюсах...

Cfyz
01.07.2026 19:50Ну так в стандарте языка С++17 auto_ptr был выпилен -- вон для кого выше написано "removed"? В списке классов стандартной библиотеки начиная с С++17 он отсутствует.
То, что GCC и clang только лишь выдают warning -- это самодеятельность компиляторов, они и кучу других отсутствущих в стандарте вещей предоставляют. MSVC код с использованием auto_ptr начиная с C++17 не компилирует.

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();
BugM
01.07.2026 19:50Ну и еще один комментарий. Автор статьи явно не понимает почему генерация случайных чисел реализована именно так (хотя вопрос не возник бы в принципе, если бы автор прошел хотя бы вводный курс математической статистики):
Зато автор пишет программы и понимает что нужно обычному программисту.
Не нужны ему все эти детали реализации в 99.99 процентах программ. Ему нужно случайное число от 0 до 100. И в удобных языках есть метод random(max_value) который дает именно то что нужно программисту в подавляющем большинстве случаев.
А все эти изыски с генераторами и особыми распределениями стоит оставить для сторонних библиотек. Их быстро напишут и все будет для тех кому надо.

Gapon65
01.07.2026 19:50"Oбычные" программисты (т.е. "джуны"), которые даже не понимают что стоит за словом "rand" скоро никому не будут нужны. Их сейчас увольняют пачками, успешно заменяя ИИ. Реально выживут (или проживут дольше) лишь инженеры с хорошим математическим образованием и архитектурным видением.
Библиотеки это, понятное дело, хорошо. Однако, для получения качественного продукта, необходимо хорошо понимать характеристики (в плане функциональности, масштабируемости и производительности) используемых библиотек. Вот даже автор статьи, похоже, понял разницу между
std::mapиstd::unordered_map.
BugM
01.07.2026 19:50Обычному сеньору тоже нужно случайное число от 0 до 100. У него все отлично и со знаниями и с пониманием. Но ему все равно нужно только случайное число от 0 до 100 и ничего больше.
Эта разница как бы не в мемах уже объясняется.
Чтобы далеко не ходить: Обычный сеньор отлично понимает зачем делать джиттер в задержках перед ретраем. И ему нужно просто случайное число для этого. А его заставляют думать не над задачей, а над какой-то ерундой.

ildarz
01.07.2026 19:50Если есть четкое понимание, что и зачем делается, что мешает просто использовать rand? Да, устаревшее, но в языке-то осталось.

BugM
01.07.2026 19:50В него правую границу передать нельзя. Наивные реализации правой границы работают неверно. Эта функция тоже не для людей сделана.

alvoskov
01.07.2026 19:50У функции rand() есть одна проблема: ни одна из её широко распространённых реализаций не подчиняется равномерному распределению. Даже вариант из glibc с большим RAND_MAX проваливает простейшие проверки вроде birthday spacings или gap test.

Nalivai
01.07.2026 19:50А потом такие вот "мне просто нужно случайное число" добавляют rand со статичным сидом в свою либу потому что "да какая разница-то чо там думать, ранд и все" и вместо джиттера получают статичную задержку

BugM
01.07.2026 19:50Эта проблема тоже давно решена. Сид должен быть необязательным параметром для инициализации.
Если не указали - вам дадут что-то случайное. И это хороший дефолт.
Если указали - будет вам стабильная последовательность. Для тестов полезно.

firegurafiku
01.07.2026 19:50Обычному сеньору […] все равно нужно только случайное число от 0 до 100
Этот же самый “обычный сеньор” будет на каждом углу доказывать, что синглтоны и скрытый глобальный стейт — это плохо и антипаттерн. Так вот, функция, возвращающая “просто случайное число” — это и есть скрытый глобальный (скорее, тред-локальный) стейт.

trinxery
01.07.2026 19:50А инженер на это ответит, что не нужно бездумно механически вычищать антипаттерны в местах, которые даже близко не являются нагруженными/генерирующими техдолг/иным образом причиняющими ущерб.

alvoskov
01.07.2026 19:50Конкретно функция rand() может причинить ущерб, если кто-то всерьёз решит, что она подчиняется равномерному распределению и её можно использовать для каких-то количественных оценок. Тут техдолг скорее в плохих алгоритмах в glibc и msvcrt.

Nick0las
01.07.2026 19:50Я как бывший научный сотрудник рад, что в стандарт затащили заумные вещи типа хорошо управляемого random и комплексных чисел. Но могли бы затащить и что-то простое, напимер так
int a = std:dummy_random_in_range<int>(-5, 5); // Случайное число от -5 то 5 включительно double f = std::dummy_random<float>(1); // случайное в интервале [0,1) (не включая 1)Но уже тут возникают вопросы: а у этого random seed будет константый (т.е. повторяемый между запусками) или случайный? И для float возможно нужны несколько реализаций, включающих и не включающих граничные значения. Видимо потому и не включили, что не смогли договориться, как надо просто.

firegurafiku
01.07.2026 19:50Ему нужно случайное число от 0 до 100
Для решения какой реальной задачи нужны (псевдо)случайные числа неизвестного происхождения с неизвестным распределением, неизвестной воспроизводимостью и неизвестной производительностью?
Генерируете ключ шифрования, IV или токен — вам нужен максимально настоящий и качественный рандом, с тем или иным источником энтропии из реального мира.
Генерируете входные воздействия для тестов или решаете что-то методом Монте-Карло — вам нужен генератор псевдослучайных чисел с определённым балансом между качеством и производительностью.
в удобных языках есть метод random(max_value)
Который, как правило, не подходит для серьёзных применений.

BugM
01.07.2026 19:50Для решения какой реальной задачи нужны (псевдо)случайные числа неизвестного происхождения с неизвестным распределением, неизвестной воспроизводимостью и неизвестной производительностью?
Именно. Равномерное распределение, обычная скорость. Эти параметры подходят для 99.99 задач где нужно случайное число.
Генерируете входные воздействия для тестов или решаете что-то методом Монте-Карло — вам нужен генератор псевдослучайных чисел с определённым балансом между качеством и производительностью.
Просто задайте сид. Один параметр и только там где он нужен.
Качество по дефолту подойдет в тех же 99.99 процентах случаев.
Генерируете ключ шифрования, IV или токен — вам нужен максимально настоящий и качественный рандом, с тем или иным источником энтропии из реального мира.
Я слишком туп чтобы писать криптографию.

playermet
01.07.2026 19:50Который, как правило, не подходит для серьёзных применений.
Более 99.9% всего кода не является "серъезным" в принципе. Да и речь же не идет о том, чтобы удалить специфичные версии. Речь о том, что должен быть удобный дефолт, а дальше уже можно делать конкретные библиотечные вариации. Сениор разберется, где ему не подходит дефолт. Джуниору никакого прока от этих специнструментов, он понятия не имееет ради чего его заставляют весь этот бойлерплейт писать.

alvoskov
01.07.2026 19:50Единственное что - если такой упрощенный дефолт в язык заводить, то целесообразно сразу требовать использование поточного шифра в качестве движка.

ReadOnlySadUser
01.07.2026 19:50Я позволю себе не согласиться.
Нет такой вещи как "просто случайное число от 0 до 100". В принципе, нет ничего простого в случайных числах и случайности вообще. Кроме ну совсем "наколеночных" случаев, но там и просто вызов rand() сойдет.
В любом месте, где требуется случайность, неизбежно возникают вопросы о распределении и о скорости/криптостойкости. Если они не возникают, значит вы не знаете что вы делаете и скорее всего допускаете ошибку/уязвимость
alvoskov
01.07.2026 19:50Весь вопрос - что такое наколеночные случаи, как это формализовать. Я границу провожу просто - rand() подходит только в том случае, если устроит даже такой ГПСЧ как
.

ReadOnlySadUser
01.07.2026 19:50Для меня "наколеночный случай" - это когда "мне просто надо, чтобы тут циферка иногда менялась, но на саму циферку мне плевать") А там хоть итератор над кольцевым буфером сделать)

alvoskov
01.07.2026 19:50Насколько я помню, в DOOM так и делали, выдавали именно из закольцованного буфера, и всё нормально было. Но для меня сущая загадка, почему в rand() до сих пор остаются плохие алгоритмы, и man-страницы даже не предупреждают об этом. Если бы к ней подходили бы как к синусу или косинусу - там бы явно крутился бы по умолчанию поточный шифр.

ReadOnlySadUser
01.07.2026 19:50С DOOM опять же не совсем корректный пример) Я не знаю как они использовали эти случайные числа, но скорее всего они озаботились о статистических свойствах циферок в кольцевом буфере.
Я говорил о совсем вырожденных случаях, когда нам просто нужно чтобы "изменения были", но мы вообще не заинтересованы в статистических свойствах этих изменений.
Например, учебные задачи или тестирование, когда нам нужно внести некий хаос в систему. Впрочем даже в последнем случае нам нужно понимать параметры этого хаоса)
Блин, реально ведь сложно придумать ситуацию, когда мы не хотим знать источник энтропии для засеивания ГПСЧ и распределение. Возможно только о криптостойскости мы иногда не хотим думать
alvoskov
01.07.2026 19:50Возможно только о криптостойскости мы иногда не хотим думать
Ну а если не хотим думать, то почему бы не использовать криптографические примитивы как ГПСЧ по умолчанию? Да, это может не быть полноценным криптогенератором, но в случае сидирования от /dev/urandom или его аналогов хотя бы не даст катастрофических последствий при использовании для генерации паролей или ключей.
Я говорил о совсем вырожденных случаях, когда нам просто нужно чтобы "изменения были"
Тогда почему бы не использовать обычный счётчик?
С учебными ситуациями и тестированием ситуация с моей точки зрения такая:
Если в учебной задаче хоть что-то вычисляется, то целесообразно считать, что в качестве ГПСЧ по умолчанию нужен поточный шифр. Компромиссы вроде MT19937 возможны, но должны осознаваться как компромиссы.
rand() для написания тестов в принципе непригодна, т.к. в стандарте языка алгоритм не указан, и на разных платформах будет разная последовательность.

ReadOnlySadUser
01.07.2026 19:50Ну, если брать конкретно rand(), то это наследие С, он компилируется где угодно даже на самой распоследней ручке, включая bare metal, и никакие требования к источнику энтропии в стандарте особо не закрепить.
В С++, впрочем, история похожая.
Ну, а на счёт криптографических примитивов всегда - ломается zero cost abstraction) Я знаю что всем наплевать, но комитет часто прикрывается этой хернёй)

alvoskov
01.07.2026 19:50Да, на bare metal с источником энтропии может быть не очень просто, тут соглашусь. Впрочем, можно было бы сделать так:
1) Внести в стандарт C требование использования поточного шифра и системного криптогенератора для инициализации в функции rand(). В стандарты C++ обязать это использовать как ГПСЧ по умолчанию в модуле random. В C++26 уже добавили почти криптографический Philox (но он - не шифр всё же), жалко, что хотя бы его не обязали использовать как генератор по умолчанию.
2) В случае отсутствия криптогенератора программа с использованием rand() должна просто не компилироваться, отключаться это должно только чем-то вроде #define USE_BAD_RANDOM_SEEDS
3) В случае невозможности использовать в rand() поточный шифр - аналогично. Компиляция должна включаться чем-то вроде #define USE_BITHACK_PRNG. И некриптографический генератор даже в этом случае должен выдерживать 128 ТиБ в PractRand и батареи TestU01.
Но насчёт криптографических примитивов: их использование в rand() - это эквивалент реализации синуса и косинуса с максимально возможной точностью. Но почему-то к одной математической функции (rand) считается допустимым куда более безалаберный подход, чем к функции sin.
Также из-за мьютексов в rand() glibc оно всё равно ощутимо медленнее AES. Считаю, что нужно либо менять ГПСЧ, либо добавлять предупреждение о низком качестве генератора в документацию glibc и man-страницах.

ReadOnlySadUser
01.07.2026 19:50Ну, на cppreference честно написано
There are no guarantees as to the quality of the random sequence produced. In the past, some implementations of
rand()have had serious shortcomings in the randomness, distribution and period of the sequence produced (in one well-known example, the low-order bit simply alternated between1and0between calls).rand()is not recommended for serious random-number generation needs. It is recommended to use C++11's random number generation facilities to replacerand().(since C++11)

alvoskov
01.07.2026 19:50Это да, в C++11 уже есть два быстрых и приличных генератора: mt19937 и mt19937_64. Когда в C++26 появится Philox - вообще хорошо станет, т.к. он и удобен для многопоточности, и все тесты проходит.
Вот бы это предупреждение из cppreference про rand() еще и в документацию glibc и man-страницы добавить.

mayorovp
01.07.2026 19:50Это плохой класс, при таком использовании слишком часто будет создаваться новый mt19937, что убивает всю идею mt19937. Если уж так делать - проще сразу
_dist(_rd)писать вnext()А если мы всё-таки используем std::mt19937, то надо делать как-то так (пишу по документации, код не проверял):
claсс Random { private: std::mt19937 _gen; public: Random() : _gen(std::random_device()()) { } int next(int minVal, maxVal) { std::uniform_int_distribution<int> dist(minVal, maxVal); return dist(_gen); } static Random& shared() { thread_local Random instance; return &instance; } };

allishappy
01.07.2026 19:50Мне так нравится эта наивная вера некоторые разработчиков, которые уверенно заявляют, что они-то вот настоящие инженеры, которых не уволят из-за ИИ. А других обязательно уволят, потому что они липовые программисты, так как не знаю особенности рандома.
На деле рыночек порешает всех по своим правилам, которые заранее нам знать не суждено. Но последние года до бума айти в среднем по больнице скорее смузихлёб на JS зарабатывал куда больше по сравнению с "настоящим" инженером на C/C++
ReadOnlySadUser
01.07.2026 19:50Ну, а "настоящий инженер С++" зарабатывает в среднем по больнице сильно больше научного сотрудника в НИИ или там даже инженера на атомной электростанции.
Значит ли это, что всех научных сотрудников и инженеров заменит ИИ?
Я могу наоборот сказать, что раз С++ программисты в среднем дешевле, значит и заменять ИИ их будут в среднем после того, как заменят всех смузихлёбов)

alvoskov
01.07.2026 19:50В том-то и проблема, что более-менее приличный std::mt19937 - не дефолтный, а что будет выбрано по умолчанию - стандарт не специфицирует (default_random_engine теоретически может быть и какой-нибудь плохой генератор вроде minstd). А вообще я считаю, что ГПСЧ по умолчанию даже для некриптографических применений должен быть основан на каком-то поточном шифре вроде AES, ChaCha или ThreeFish.

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

MountainGoat
01.07.2026 19:50Не. У меня дед всю жизнь проездил на Запорожце, был полностью доволен, любил его даже: раз в неделю в нём что-то смазывал. /s

yurrig
01.07.2026 19:50Ну уж если с авто сравнивать, C++ - это, скорее, Феррари. Водить надо уметь, но гоняет - другим не чета.

MountainGoat
01.07.2026 19:50Сильно сомневаюсь, что он может быть быстрее любого другого компилируемого неуправляемого языка. Да и нужно это гораздо реже, чем кажется.

yurrig
01.07.2026 19:50Оптимизатор у хорошего C++ компайлера даст 100 очков вперед другим языкам. Перформанс, по крайней мере, в моей области (электронные CAD-ы), важен примерно всегда, и почти всегда - узкое место.

VladimirFarshatov
01.07.2026 19:50Но не всем. Watcom C Compiler обойдет на 10 очков вперед. )))

yurrig
01.07.2026 19:50Исключение, подтверждающее правило) Watcom - да, классный. Правда, я с ним не работал уже давно..

sacred1972
01.07.2026 19:50Ни одно исключение не подтверждает ни одного правила. Любое исключение из правила - опровергает правило. Возьмём правило: “у треугольника все углы острые”. И сразу легко находим треугольники-исключения, у которых один угол тупой или прямой. Правило опровергнуто, а никак не “подтверждено”. Люди, применяйте правильные поговорки. Неправильные не применяйте. Правильная поговорка (вернее это изречение, ни много ни мало - Цицерона) звучит (примерно) так: существование исключения, свидетельствует, что существует и правило, из которого сделано это исключение. Согласитесь, смысл совершенно другой, и вполне логичный. Ну, как бы, Цицерон был умный дядька. /зануда mode off

playermet
01.07.2026 19:50С++ это набор запчастей, из которых можно собрать ржавое ведро, а можно самолет в цветах Феррари. Зависит от того, что набрать, а что выкинуть.

alex_marshal
01.07.2026 19:50Существует пласт системного ПО, созданного исключительно на ассемблере:
KolibriOS (и MenuetOS) - полноценные и современные операционные системы, которые включая ядро, драйверы, текстовые и графические редакторы, игры и браузер, написаны на ассемблере (FASM). При этом KolibriOS помещается на одну дискету, загружается за пару секунд и мгновенно работает даже на древних ПК.
FASM (Flat Assembler) - популярный современный компилятор ассемблера, который сам полностью написан на ассемблере. Он самостоятельно компилирует свой собственный код.
В сфере защиты данных ассемблер незаменим для предотвращения атак по сторонним каналам и ускорения математических вычислений:
OpenSSL / BoringSSL - криптографические библиотеки, обеспечивающие безопасность почти всего современного интернета (HTTPS). Самые ресурсоемкие алгоритмы шифрования (AES, RSA, ChaCha20) написаны на чистом ассемблере x86-64 и ARM для задействования аппаратного ускорения процессоров.
WireGuard - современный и быстрый протокол VPN. Его криптографическая база во многом опирается на оптимизированный вручную ассемблерный код.
Обработка видео и аудио в реальном времени требует колоссальной вычислительной мощности:
FFmpeg / x264 / x265 - движки, на которых работает практически всё современное цифровое видео (включая плееры VLC, YouTube, стриминги). Сверхбыстрое сжатие и декодирование видеопотоков обеспечивается за счет сотен тысяч строк кода, написанных на ассемблере под SIMD-инструкции (AVX-512, SSE, NEON).
FLAC / WebM - аудио и видеокодеки содержат низкоуровневые ассемблерные оптимизации для минимизации задержек при воспроизведении.
Можно продолжать ещё долго. Так что да, статья "обиженного джуна" из ютуба вполне имеет право на жизнь.

JBFW
01.07.2026 19:50У ассамблера есть маленький недостаток - он камнезависимый.
Поэтому получится, что тот же ffmpeg на Intel и ffmpeg на Arm64 должен использовать разные коды, а в некоторых случаях и передавать выполнение встроенным в камень же аппаратным x264-декодерам.
Поэтому не везде можно применять ассемблер.

MountainGoat
01.07.2026 19:50Ладно там ARM. Код на P и E ядрах одного и того же Intel процессора должен использовать разный ассемблер. Иначе P ядра не выйдут на всю силу. Что и было частью провала этой архитектуры: Intel компилятор так умеет, а остальные оси и компилируемые языки не очень.

alex_marshal
01.07.2026 19:50я выложил все плюсы Ассемблера как альтернативу а мне за это минусов налепили и карму скинули, маньячиллы, ну и облизывайте свой C++ как конфету, тоже мне, умники...

ReadOnlySadUser
01.07.2026 19:50Но у asm-а нет никаких плюсов) Вообще никаких) Я даже отброшу мою субъективную неприязнь к внешнему виду ассемблера и что код на нём - это слишпиеся макарошки :)
Вот список очевидных недостатков
1. Каждая команда - это считай что ключевое слово в языке. Многие ли способны запомнить набор команд для x86_64? А сразу для ARM и x86_64? Да ещё и для всей линейки процессоров. Это делает чистый assembler космически сложным языком просто для запоминания.
2. Современные процессоры давно уже все Out-of-Order, а следовательно и инструкции надо упаковывать так, чтобы оно работало с учётом устройства внутрянки каждого отдельного процессора.
3. Использование ассемблера требует нефига нетривиальных познаний в архитектуре ЭВМ. Даже данные правильно выравнивать будет нафиговой такой когнитивной нагрузкой.
Я не говорю, что у ассемблера нет применений. Я сам писал на нём крипту и переписывание "слово в слово" с чистого Си на ассемблер дало прирост в 15% на ровном месте. И в этом ассемблер оч хорош - числодробилки и общение с железом. Но в массе свой он нахрен не нужен) Никому)
Siemargl
01.07.2026 19:501й пункт неточен, поскольку макроассемблеры изобрели оочень давно, еще до С.
А в остальном - современный компилятор С итп подбирает инструкции и их порядок лучше человека.

ReadOnlySadUser
01.07.2026 19:50Это уточнение вскрывает ещё одну проблему: диалекты ASM :) Целый зоопарк диалектов.
В чем смысл всё это учить и помнить, если есть Си)

Siemargl
01.07.2026 19:50C++ это такой язык и оптимизатор, что иногда проще заглянуть в ассемблер "что же получилось" =)

domix32
01.07.2026 19:50KolibriOS (и MenuetOS) - полноценные и современные операционные системы
Они не очень современные и совершенно точно не полноценные. Особенно учитывая, что любой код исполняется от уровня ядра - из-за отсутвия хотя бы базового разделения на kernel/userspace и aslr любой блокнот ваши пароли сольёт кому угодно. Гуй прибит гвоздями, usb-стек минимальный и про 3.0 ничего не знает, про bluetooth вроде вообще не задумывались, не говоря уже про BLE; туда же современный WiFi, NVME, GPU. С учётом отсутвия слоев совместимости с Linux, BSD или хотя бы POSIX внешние драйвера или иной софт уровнем повыше они также не умеют запускать. Виртуализация, дедупликация зависимостей, высокоуровневых API для каких-то оптимизированных вычислений также нет. Оно прикольно как POC, но совершенно точное не как современная ОС. Сделать блокнот, paint или солитера вообще не проблема, те ещё на дисковых ОС были.
Сказ про флоппик хорош как маркетинг для привлечения внимания, но не как selling point - вы с тем же успехом можете впихнуть ядро линукса на флешку, повыкидывав кучу драйверов, оставив примерно столько же, сколько их в колябрии есть и докинуть туда драйвер для видюхи и какой-нибудь hyperland для красоты включить в состав.

capslocky
01.07.2026 19:50Линус Торвальдс много раз объяснял почему Linux на C и в нём никогда не будет C++. И одна из причин было то, что он не хочет, чтобы код в его проекте писали C++ программисты. При этом Rust был им допущен и уже есть в исходниках Linux.

jaha33
01.07.2026 19:50Практически в любой теме по c++ к комментариях цепляются несколько человек, которые чуть ли не глотки грызут друг другу, доказывая что в плюсах правильно, что нет, как им правильно пользоваться/какими фреймворками и т.д.
Даже представить не могу как плюсовики для продакшена что то обсуждают и решают. И при этом язык пережил уже многих "убийц" и помирать не собирается

blind_oracle
01.07.2026 19:50А он и не помрёт, ибо дофига легаси. И даже новые проекты активно появляются. Плюс весь геймдев.
Но ниша его сужается активно. Го вытеснил из сервисов всяких и веба, Раст из системного программирования потихоньку давит.

Astrowalk
01.07.2026 19:50Не только из системного. Например, вымерли или были переписаны многие блокчейны, написанные не на Rust. Новые проекты в областях, где требуется быстрое создание системы разработчиками среднего уровня в сочетании с требованиями повышенной надёжности и секурности, практически безальтернативно начинаются на Rust.

playermet
01.07.2026 19:50Очень просто. В каждой фирме свой собственный диалект и кодстайлгайд С++, не совместимый со всеми остальными. Внутри одного коллектива со временем притираются, а когда через интернет контактируют два таких коллектива - получаем наблюдаемый эффект.

Jijiki
01.07.2026 19:50только небольшая поправочка, в С и Раст это определенный подход в котором нету нормального наследования, поэтому С++ не вытиснить, он органично вписывается куда надо, там где на С и Расте будет портянка нечитаемого кода с багованным наследованием(Раст от одного этого тейка или недоделки уже отлетает просто осознанно).

bromzh
01.07.2026 19:50в котором нету нормального наследования
А оно точно надо? Вот прям никуда без него?

Nalivai
01.07.2026 19:50Ну если так рассуждать, то ничего точно не надо, и без всего можно обойтись в общем-то. Тут вон призывают все на Асме писать, и технически не невозможно же.

Medeyko
01.07.2026 19:50Я думаю, коллега подразумевал, что "нормальное наследование" - проблемная штука. Как goto, типа. Вспомните Банду четырёх, в частности: "Favor object composition over class inheritance". Такое наследование - удобный инструментарий, чтобы "выстрелить себе в ногу", и написать код, который сложно поддерживать и развивать.
В ранних версиях Rust'а было наследование в том смысле, который Вы вкладываете. От него сознательно отказались.
А так, наследование в духе интерфейсов в Rust'е есть.

Nalivai
01.07.2026 19:50Это не первое и не последнее в плюсах, что позволяет себе ногу просто-таки нашпиговать свинцом. Плюсы это язык который построен на принципах полной доступности всех инструментов, а програмист сам дурак если что. За то и любим.

Siemargl
01.07.2026 19:50Иногда надо, иногда нет.
А тут выбор уже сделали за вас.

bromzh
01.07.2026 19:50Ну можно в язык тогда всё впихнуть, чтобы было всё на всякий случай жизни. Ну вдруг пригодится. Например, вот нужно 20 способов инициализации. Обычно парочка нужна, но иногда прям жить невозможно без других 18-ти. Или ромбовидное наследование, некоторые задачи прям нерешаемы без него.
Ой, да это же получился...

Siemargl
01.07.2026 19:50Это все называется эволюция.
Конечно, в современных языках этот путь уже учтен.

Medeyko
01.07.2026 19:50Да, и это как раз фишка языка - минимизировать количество способов выстрелить себе в ногу. Часто считают, будто Rust характерен только системой владения памятью, но на самом деле использование безопасных подходов для работы с памятью - это всего лишь один из элементов (хоть, безусловно, и самый заметный) общей стратегии.
Так, ещё в Rust нет goto. А ведь оно тоже, наверное, "иногда надо, иногда нет"?

NeoCode2
01.07.2026 19:50Неистово плюсую, всё верно. И еще вот это всё привело к тому что язык стал... неэстетичным, что-ли. Вот к примеру std::move - почему такая громоздкая конструкция, если по смыслу это вообще должен быть унарный оператор? А есть еще std::forward, std::move_if_noexcept, std::decay, std::launder, std::common_type, std::remove_reference, std::remove_const, std::enable_if, std::conditional и прочие is_trivially_default_constructible. Особенно если все это навёрнуто в трехэтажных шаблонах.
Всё это выглядит как огромное количество "кишок наружу" - какие-то шаблонные конструции, которые вроде как и нужны (наверное, мне ни разу не понадобились), но... в них не хочется углубляться. На языке хочется писать прикладные программы, а не заниматься странными хаками компилятора.
В качестве контрпримера могу привести библиотеку Qt - огромная библиотека, но всё строго и по делу. Приятный синтаксис, богатые классы, решающие реальные прикладные задачи. Шаблоны используются строго для того, для чего они и задумывались - параметризация контейнеров. Даже camelCase выглядит приятнее чем snake_case.

DerTosser
01.07.2026 19:50Я что-то путаю или можно после заголовочных файлов написать
using namespace std;и потом не использоватьstd::перед каждым чихом?
artptr86
01.07.2026 19:50Можно, но тогда в текущем пространстве имён внезапно могут появиться move, data, begin, end, size, span, list, map, queue, erase и прочие однословные классы и функции.

Aldrog
01.07.2026 19:50И этот список ещё и будет меняться от компилятора к компилятору и от версии к версии. Потому что стандарт не даёт никаких гарантий относительно того, какие символы не прилетают с включением какого-либо стандартного заголовочника. И по факту это разнится очень сильно.

Contender
01.07.2026 19:50Отсюда vector, потому что в численных вычислениях Scheme «вектор» означал одномерный непрерывный массив, так что в контексте Степанова имя было логичным. Потом спохватились и хотели назвать array , но имя уже примелькалось в стандарте и проектах, поэтому переименовать опять было нельзя, опять тоже самое, что и везде в языке.
На самом деле понятие вектор использовалось в математике и означало частный случай матрицы - либо строку (вектор-строка), либо столбец (вектор-столбец).

mayorovp
01.07.2026 19:50С матрицами та же проблема, это вовсе не синоним двумерному массиву - однако программисты зачастую про это забывают.

Nick0las
01.07.2026 19:50Спасибо за статью. Хоть в основном все вышеперечисленное знаю, почитать было интересно. C++ действительно не самый быстрый но самый совместимый. Но он настолько разнообразен, что на нем можно писать так, чтобы было быстро.
Но я бы упомянул еще несколько моментов:
В вариантах целочисленных типов не упомянуты size_t без std и intptr_t.
Название vector - вполне нормальное название для массива элементов на мой взгляд. Вектор в линейной алгебре будучи разложенным по некоторому базису как раз и представляет собой массив.
Самая большая боль C++ это undefined behavior. И дело даже не в разименовании неинициализированного или деаллоцированного указателя, а в том как компиляторы по факту трактуют стандарт. Если в стандарте написанно что XXX может производить - это UB, а разработчики стандарта считают что программист пишет без UB и делают соответствующие предположения о коде. Например известная история с переполнением Int.
Есть еще одна проблема c и c++, это strict aliasing rules. Дополнительные правила, которые усложняют каст указателя на один тип к другому чтобы поковыряться в битах.
Еще можно было бы сказать про расширения gcc которых не хватает в стандарте, но пожалуй пока хватит.

mayorovp
01.07.2026 19:50В том-то и суть, что вектор надо сначала разложить по базису, чтобы массив координат получить. А разложив его по другому базису - можно получить другой массив координат, при этом вектор будет тот же самый.
В плюсах же для большинства векторов базис, который имел бы смысл, и придумать-то не получится.

Johnny_Depp
01.07.2026 19:50В стандарте C++ четко сказано: "Переменная вводится в программу объявлением (declaration) и имеет идентификатор (имя)".
это когда же у нас вызов функции и передача аргумента вдруг стало причиной инициализации переменной?
Конечно иногда аргументы функции передаются через стек, но в основном через регистры, и из-за этого считать что аргумент это вдруг отдельная переменная - как минимум странно. Да, конечно если рассматривать передачу через стек, то механизм схож с локальной переменной, но это всё равно другое. Ни разу не слышал, чтобы хоть кто-то из тех же реверс-инженеров говорил что аргумент функции это переменная.
struct S {
int x = 5;
int y{10};
S() : y{2}, x(1) {}
};
ВОТ ЭТО, ни как не переменная, сама структура (её описание) - это тип данных (чертеж), а переменная - это экземпляр (конкретный объект в памяти).
return x;
return {1, 2, 3};
throw x;
И это вообще и близко, тоже не переменные.
"Простые вещи просто не делаются"
Ну оберни в функцию и будет тебе всё по одной кнопке? За чем своё субъективное ощущение после условного питона(или джавы), выдавать за некую справедливую оценку языка?
"Проблемы кастинга"
Тут автор буквально пишет, что после джавы, ему тяжело запомнить приведение типов к другим типам и так же показывает, что читать в интернете зачем нужен каждый он не хочет, а зря, узнал бы зачем они нужны.
Короче дальше мне стало нудно читать ибо:
1. Автор выдаёт субъективное ощущение за объективное оценивание2.Автор так же явно начал трогать С++, после более высокоуровневого языка (той же джавы которую он активно упоминает, что много даёт понять) , при этом открывать документацию или какие-то статьи он не хочет, возможно где-то пробежался, нахватался и теперь ноет на хабре.

mayorovp
01.07.2026 19:50Аргумент функции - и правда не переменная, а вот параметр (формальный) обладает всеми признаками переменной: имя, возможность присвоить значение, возможность получить адрес.
А на стеке она, в регистре или вовсе оптимизирована в ноль - нет разницы.
Кроме того, в математике параметры функций открыто называют переменными, и само слово “переменная” именно из математики и пришло.

rukhi7
01.07.2026 19:50ну да, перефразируя известное высказывание:
Демократия — наихудшая форма правления, если не считать всех остальных, которые пробовались время от времени
можно сказать
С++ — наихудший язык программирования, если не считать всех остальных, которые пробовались время от времени
С++ это язык именно программирования, именно для программистов, а не для физиков, лириков, ...
Программисты должны понимать откуда взялся, что значит, и почему он именно static, например.

trinxery
01.07.2026 19:50Мне так нравится эта наивная вера некоторые разработчиков, которые уверенно заявляют, что они-то вот настоящие инженеры, которых не уволят из-за ИИ. А других обязательно уволят, потому что они липовые программисты, так как не знаю особенности рандома.
На деле рыночек порешает всех по своим правилам, которые заранее нам знать не суждено. Но последние года до бума айти в среднем по больнице скорее смузихлёб на JS зарабатывал куда больше по сравнению с "настоящим" инженером на C/C++
via https://habr.com/ru/articles/1047890/comments/#comment_30177360
Интересно будет послушать ваш анализ Rust, почему он не "именно язык программирования для программистов".

Johnny_Depp
01.07.2026 19:50Ну как минимум потому что Rust выглядит как очередная кривая обёртка над С++.
Вот две функции для записи в память другого процесса:
RUST:fn write_memory(handle: HANDLE, address: usize, data: &[u8]) -> Result<usize, Error> { let mut bytes_written = 0usize; unsafe { WriteProcessMemory( handle, address as *mut _, data.as_ptr() as *const _, data.len(), Some(&mut bytes_written), )?; } Ok(bytes_written) }C++:
SIZE_T WriteProcessMemoryEx(HANDLE hProcess, LPVOID address, LPCVOID buffer, SIZE_T size) { SIZE_T bytesWritten = 0; WriteProcessMemory(hProcess, address, buffer, size, &bytesWritten); return bytesWritten; }Вам нужно объяснять? Или вы проведёте сравнение и увидите простоту и лёгкость С++ в базовой задаче?
Вы действительно считаете что RUST выглядит и есть лучше чем C++ ?
P.s. На меня накинулись фанаты Rust, у которых от их языка уже проблемы с логикой, так как они не понимают что тут слова про память просто пример, а не аргумент
Cerberuser
01.07.2026 19:50Вопрос в том, что считать “базовой задачей”. Для тех, кто предпочитает Rust, описанное вами - не базовая задача, а редкий случай, который надо сделать один раз, протестировать как следует и больше на него не смотреть. Для тех, кто предпочитает C++ - вероятно, наоборот.

Johnny_Depp
01.07.2026 19:50Так в смысле, обёртка над функцией не базовая задача? Одно из самых частых это обёртка над функцией с некоторой логикой.
Я показал пример обёртки над функцией, можно даже не знать что она делает(функция).
Смысл в том, что в С++ всё красиво и аккуратно, не каких лишних букв и символ.
Вот зачем отделять тип и словесный ид двоеточием, что это за порнография?-> Result<usize, Error>
А вот что это за порнография ??
Cerberuser
01.07.2026 19:50обёртка над функцией не базовая задача?
Над функцией, которая лезет напрямую в WinAPI (и вообще в сторонний C API)? Не для всех уж точно.
Вот зачем отделять тип и словесный ид двоеточием, что это за порнография?
А. То есть вопрос на самом деле вообще не в “кривой обёрточности”, а просто в том, что синтаксис некрасивый?

trinxery
01.07.2026 19:50Одно из самых частых это обёртка над функцией с некоторой логикой.
Только обычно логики 90%, а бойлерплейта 10% (условно), в вашем примере 0% и 100% соответственно.
в С++ всё красиво и аккуратно, не каких лишних букв и символ
Вы тут де-факто на Си пишете. Почему указатель + длина, а не std::span?
Вот зачем отделять тип и словесный ид двоеточием, что это за порнография?
Мысль гласит, что это ради того, чтобы не было несколько способов делать одно и то же. К тому же, много новых языков уже выбрали такой способ:
https://habr.com/ru/articles/532660/comments/#comment_22487588 (также https://habr.com/ru/articles/532660/comments/#comment_22487564, https://habr.com/ru/articles/532660/comments/#comment_22488240)

ZirakZigil
01.07.2026 19:50А вот что это за порнография ??
Это std::expected<std::size_t, your_error_type>.

playermet
01.07.2026 19:50Смысл в том, что в С++ всё красиво и аккуратно, не каких лишних букв и символ.
Серьезно? С++ один из самых ругаемых языков за непонятную мешанину из спецсимволов.

trinxery
01.07.2026 19:50Действительно, ведь 95% целевой аудитории занимаются только тем, что пишут в память других процессов на Win32.
Функция на Rust сразу даёт информацию об успешности завершения и возвращает информацию об ошибке, если есть, когда как в C++-версии это придётся делать вызываемому коду. Во-вторых, ясное дело, что если вы будете вызывать из языка, где принято использовать другие, не Си-подобные приёмы, тонкую обёртку над сишной функцией, у вас будет некрасивый обвязочный код.

Johnny_Depp
01.07.2026 19:50"Во-вторых, ясное дело, что если вы будете вызывать из языка, где принято использовать другие, не Си-подобные приёмы, тонкую обёртку над сишной функцией, у вас будет некрасивый обвязочный код."
Ну то есть, вы буквально говорите, что если не си-подобный приём, то будет не красиво.
ДА ПРИ ЧЁМ ТУТ ПАМЯТЬ, ЧТО ВЫ К НЕЙ ПРИСТАЛИ, ЭТО ПРОСТО КАК ПРИМЕР.
Вот сразу видно любителей Rust и т.п. У них проблемы с логикой, тут не важно что делает функция которую обернули, это просто пример.
trinxery
01.07.2026 19:50Ну то есть, вы буквально говорите, что если не си-подобный приём, то будет не красиво.
Я такого не говорил ("Вот сразу видно любителей C++ и т.п. У них проблемы с логикой"), я писал, что вызовы из одного языка программирования функций из другого языка программирования обречены иметь бойлерплейт и прочий некрасивый код. Вы же из одного такого примера пытаетесь вывести, что весь Rust плохой, а C++ хороший.
ДА ПРИ ЧЁМ ТУТ ПАМЯТЬ, ЧТО ВЫ К НЕЙ ПРИСТАЛИ, ЭТО ПРОСТО КАК ПРИМЕР.
Успокойтесь, к памяти никто не приставал, я даже этого слова не писал. Вместо WriteProcessMemory могло быть большинство других сишных функций, и все аргументы остались бы действительны.
У них проблемы с логикой
Это у вас проблемы, по одному нишевому примеру судить о всём языке.

Johnny_Depp
01.07.2026 19:50Ну да, если простая обёртка выглядит криво, то функция со сложной логикой точно будет хорошо.
Гениально.
"Во-вторых, ясное дело, что если вы будете вызывать из языка, где принято использовать другие, не Си-подобные приёмы, тонкую обёртку над сишной функцией, у вас будет некрасивый обвязочный код."
"Во-вторых, ясное дело, что если вы будете вызывать из языка"
что вызывать то?
"где принято использовать другие, не Си-подобные приёмы"
А почему? Си-язык, возник для удобств, что бы на ассемблере не строчить, и ни кто после ассемблерных мнемоник не хотел перегружать язык кучей лишних символов. То есть си-стайл уже сам по себе имеет девиз "Просто и логично" , но нет надо ведь всё испортить
"тонкую обёртку над сишной функцией, у вас будет некрасивый обвязочный код"
Ну да, конфета плохая не из-за производства, а из-за фантика, я вас понял.
Это я повторно ответил, потому что вы теперь отпираетесь и говорите что я вас не так понял.
Некрасивый обвязочный код, сугубо из-за того что сам язык такой
trinxery
01.07.2026 19:50Ну да, если простая обёртка выглядит криво, то функция со сложной логикой точно будет хорошо.
Гениально.
(Выделение моё) Не "точно" -- из "обёртка -- криво" не следует "сложная логика -- хорошо", но! из "обёртка -- криво" не следует "сложная логика -- криво". Если вы хотите сравнить качество двух языков, то сравнивайте их на наборе примеров и использований, которые соответствуют их применениям в реальности.
"Во-вторых, ясное дело, что если вы будете вызывать из языка, где принято использовать другие, не Си-подобные приёмы, тонкую обёртку над сишной функцией, у вас будет некрасивый обвязочный код."
"Во-вторых, ясное дело, что если вы будете вызывать из языка"
что вызывать то?
(Выделение моё)
"где принято использовать другие, не Си-подобные приёмы"
А почему? Си-язык, возник для удобств, что бы на ассемблере не строчить, и ни кто после ассемблерных мнемоник не хотел перегружать язык кучей лишних символов. То есть си-стайл уже сам по себе имеет девиз "Просто и логично" , но нет надо ведь всё испортить
Не "просто и логично", а скорее "примитивно". Язык программирования обязан не только отражать намерения разработчика в машинный код, но и обеспечивать, по мере своих сил, поддерживаемость, безопасность кода и т.п.
Защищать Rust легко, так как это новый язык, у которого была возможность учесть весь опыт других языков, но вы к этому аргументу не аппелируете.
но нет надо ведь всё испортить
Это вы про C++?~
"тонкую обёртку над сишной функцией, у вас будет некрасивый обвязочный код"
Ну да, конфета плохая не из-за производства, а из-за фантика, я вас понял.
Это я повторно ответил, потому что вы теперь отпираетесь и говорите что я вас не так понял.
Некрасивый обвязочный код, сугубо из-за того что сам язык такой
Что было: в коде на Rust функции из C++ вызываются некрасиво;
Ваше обобщение: в коде на языке X функции из языка Y вызываются некрасиво => X -- плохой язык.Проведите мысленный эксперимент, в котором вы из C/C++/любого другого языка вызываете код на Rust/Python/Java/любого другого языка, и поймите, почему такой подход неверен.

Johnny_Depp
01.07.2026 19:50Ладно пойдём от обратного, примитивно, означает что всё прозрачно и понятно, "Если вам нужно сделать шаг вперёд, вы просто его делаете, вам не нужно делать кульбит", С++ как раз про это, он даёт решение многих действий , и эту же философию продолжают те кто разрабатывают на нём. Вот функция читает с файла, если не смогла прочитать, возвращает код ошибки, она не пытается сама завершить весь процесс и т.п.
Кстати единственно что не нравится это отсутствие типа данных для HEX чисел, приходится через указатель, но в RUST его тоже нет.
Вот сама идея функции: данные из вне могут быть, а могут и нет, выполняет некий код, и есть возможность вывода данных. Всё. Больше ни каких прикруток не нужных. Проще уже не сделать, а попытки псевдо-оптимизации путём прикручивания каких-то там состояний после выполнения функций, зачастую делают только хуже.
Вот про это были Си, и теперь же С++, упростить это нельзя.
И вот отсюда и простая реализация, указываешь есть ли данные на выходе, название функции и перечисление аргументов функции их тип и название. ВСЁ. Что вы туда запихнуть пытаетесь? Если ничего, так зачем порождать миллион языков, которые что то там пытаются?
trinxery
01.07.2026 19:50примитивно, означает что всё прозрачно и понятно, "Если вам нужно сделать шаг вперёд, вы просто его делаете, вам не нужно делать кульбит", С++ как раз про это
Это, может быть, про Си, но не про C++:
decltype(auto) foo() { int i = 1; return (i); // UB // return i; // Нет UB }Обычно на все трудности С++ ответом является: программист должен это знать/никогда так не делайте и т.п., но эти аргументы верны для любого языка программирования вообще, так как представляют себе "бесконечно опытного" программиста и игнорируют необходимость обучения и то, что дизайн языка бывает более или менее качественным.
Вот функция читает с файла, если не смогла прочитать, возвращает код ошибки, она не пытается сама завершить весь процесс и т.п.
Ответственно заявляю, что в Rust именно такой подход и принят, и поддерживается он сильно в большей степени, чем в C++, так как в Rust изначально были Result и Option, синтаксический сахар, pattern matching и прочее, до чего C/C++ с errno и его аналогами и out-параметрами как до Луны, а std::optional и std::expected поддерживаются слабо.
Вот сама идея функции: данные из вне могут быть, а могут и нет, выполняет некий код, и есть возможность вывода данных. Всё. Больше ни каких прикруток не нужных. Проще уже не сделать, а попытки псевдо-оптимизации путём прикручивания каких-то там состояний после выполнения функций, зачастую делают только хуже.
?
<...> ВСЁ. Что вы туда запихнуть пытаетесь? Если ничего, так зачем порождать миллион языков, которые что то там пытаются?
Если вы довольны С++, то вас никто не убеждает перейти на что-то другое, но не вступайте тогда в дискуссии, если не интересуетесь темой обсуждения.

Johnny_Depp
01.07.2026 19:50decltype(auto) foo() {
int i = 1;
return (i);
}
Вы где такое увидели вообще?

trinxery
01.07.2026 19:50Вот и аргумент "никогда так не делайте". Этот фрагмент кода опровергает 'примитивно, означает что всё прозрачно и понятно, "Если вам нужно сделать шаг вперёд, вы просто его делаете, вам не нужно делать кульбит", С++ как раз про это'; также показывает неочевидность синтаксиса -- кто подумает, что
(x)иxэто семантически разные вещи.

Johnny_Depp
01.07.2026 19:50Я сейчас про то, где вы нашли этот кусок кода, я просматривал куча проектов на плюсах, сам пишу, ни кто так, никогда не пишет, а встретить тип auto это чудо, а подобный конструкт тем более.
Все всегда пишут:
int foo() {
int i = 1;
return 1;
}
И так всегда и во всём, в том числе в коде от майкрософт.
Не нужно думать раз язык что-то позволяет, то ним так и пользуются

trinxery
01.07.2026 19:50где вы нашли этот кусок кода
Он искуственный, да. https://habr.com/ru/articles/350186/comments/#comment_10693598
Не нужно думать раз язык что-то позволяет, то ним так и пользуются
Язык должен допускать как можно меньше возможностей сделать что-то не так -- это одно из свойств, определяющих, насколько язык хорошо или плохо спроектирован. Подумайте, почему на разных проектах используют линтеры, более строгие флаги компилятора, почему в Python завезли аннотации типов и инструменты для статического анализа, а вместо JavaScript во многом сейчас пишут на TypeScript.

Johnny_Depp
01.07.2026 19:50Так, если в языке что-то возможно, это добавили по самым разным причинам, в первую очередь, гибкость.
Если вы или ещё кто-то, психически болен, и начинает усложнять конструкты, то кто вам виноват?
Попробуйте на языке ПРОГРАММИРОВАТЬ, а не искать извращённые способы чтобы потом кидаться ними в комментариях.
А единственный явный плюс у RUST, который выделяют это Borrow Checker . Ну то есть снова кто-то не осилил указатели и пошёл писать очередной сборщик мусора, но продвинутый.
То есть вся слава языка просто из-за этого.
Ну то есть были те, кто не смогли запомнить простое правило, написал new, напиши delete , или calloc/malloc, напиши freeА теперь самое интересное, внешние библиотеки ваша фича не проверяет, интересно сколько времени уйдёт у майкрософт на переписывание DirectX с плюсов на RUST ?

Medeyko
01.07.2026 19:50А единственный явный плюс у RUST, который выделяют это Borrow Checker . Ну то есть снова кто-то не осилил указатели и пошёл писать очередной сборщик мусора, но продвинутый.
У меня для Вас то ли страшная, то ли прекрасная новость: Borrow Checker - это не сборщик мусора ни в каком виде - ни продвинутый, ни отсталый. Его задача - не давать некорректно работать с памятью, а не мусор собирать.
А RAII в Расте есть, конечно, куда ж без него, только прямого отношения к Borrow Checker это не имеет. RAII и в C++ есть...

Johnny_Depp
01.07.2026 19:50Ну надо ведь юмор уметь понимать.
Вдумайтесь в то что делает Borrow Checker и поймёт при чём тут "Сборщик мусора", мусор, в нашем случае это то, что не корректно работает с памятью, ну а сборщик, это тот алгоритм что собирает такой код, и показывает нам где это.
Получается сборщик мусора, и да, раз вы такой грамотный то RAII не сборщик мусора, это идея что бы в конструктор выделение памяти, а в деконструктор её освобождение

Medeyko
01.07.2026 19:50"Деконструктор"? Вы, наверное, имели в виду "деструктор"? Ну, конечно же, RAII - это не сборщик мусора, но автоматическое освобождение ресурса по выходу связанных с ним идентификаторов из области видимости - это самое близко к сборке мусора, что я смог найти, размышляя над Вашей шуткой.
То, что под сборкой мусора Вы подразумевали алгоритм, выявляющий ошибки программиста, простите, не догадался, и даже после Вашего объяснения эта Ваша мысль мне кажется слишком сильно натянутой на глобус совой...

ZirakZigil
01.07.2026 19:50Такое может получиться при разворачивании макроса (которые по известным причинам свои параметры в скобки оборачивают) который из-за ifdef'ов и/или других макрсов, которые он использует, в одном случае раскрывается в какое-то выражение, а в другом просто в (arg).

kmatveev
01.07.2026 19:50На Java, C# и Python обёртки вокруг C-шных функций из Win32 API будут выглядеть примерно так же. Красота самих обёрточных функций никого не волнует. Зато Rust-функция принимает на один параметр меньше.

misha_erementchouk
01.07.2026 19:50На один параметр меньше, потому что функциональность неодинаковая.

misha_erementchouk
01.07.2026 19:50Мне война языков по барабану, но пример явно неудачный. При взгляде на код на Расте я сразу вижу, что если записать в память не удалось, наверх побежала ошибка, которую нельзя не обработать. А в коде на С++ я вообще не вижу, что происходит в случае ошибки. Гугл говорит, что
WriteProcessMemoryвозвращает BOOL, ну и т.д.
Johnny_Depp
01.07.2026 19:50У WinAPI своя логика, подробная ошибка, вот что бы прям очень подробно, получаем через GetLastError, а так возвращение BOOL указывает на то успешно завершилась функция или нет.
Если не успешно, то дёргаем GetLastError , но это вообще не С++, а библиотека на нём
misha_erementchouk
01.07.2026 19:50Код на С++ не проверяет возвращаемое значение. Откуда станет известно, что произошла ошибка?

rukhi7
01.07.2026 19:50про Rust не могу ничего сказать, ни разу не приходилось сталкиваться с каким-то кодом на Rust. Обрывки видел какие-то 5-10 строк, но ни разу не видел какого-то законченного алгоритма или решения задачи предметной области. Может это мне так не повезло, а может это отражение какой-то глобальной статистики на меня, я не знаю! В любом случае мое мнение совершенно субъективно, прошу воспринимать его именно так.
Кстати автор высказывания про демократию тоже наверно был не особо в курсе каких-то особенностей социализма, например, это же не значит что ему надо было запретить так высказываться.

trinxery
01.07.2026 19:50Вывод: если вы не имеете оснований считать, что исследовали тему, не высказывайте своё мнение так резко. /thread

rukhi7
01.07.2026 19:50а в чем резкость-то? В том что я маленько историю знаю и могу проводить аналогии?
Если взялись советовать - советуйте до конца, иначе какой смысл в ваших советах?

trinxery
01.07.2026 19:50аналогии
Систему новую и более лучшую, чем демократия, не нашли. Новых и более лучших (в зависимости от обстоятельств) языков программирования со времён C++ придумано достаточно -- аналогия некорректна. Применение широко известных цитат к какому-либо другому явлению (как сделали вы) заявляет об уверенности автора в его суждении.
Сказав "С++ это язык именно программирования, именно для программистов, а не для физиков, лириков. Программисты должны понимать откуда взялся, что значит, и почему он именно static, например" (выделение моё) вы проводите соответствие: пишет на C++ -- программист, не на C++ -- не программист. При том, что второе может быть верно, и языки "для физиков" тоже есть, вы утверждаете, что нет языков программирования, при работе с которыми "программисты должны понимать откуда взялся, что значит, и почему он именно static, например", и забываете тех, кто знает теорию, но может на C++ и не писать.
резко
Забыл слово "уверенно". Подбирал по значению "не оставляя простора для возражений, градаций; деля на 'настоящих' и 'ненастоящих'".

rukhi7
01.07.2026 19:50Но я действительно уверен в том что написал! Я также уверен что есть что-то в чем уверены вы и с чем я не смогу согласиться, но я бы не стал вам советовать в чем вы должны быть уверены в любом случае! Это и есть основа холиваров - священных войн, насколько я знаю.
Если вам интересно по поводу С++ , то у меня есть статистика по C#, например, что C# намного проще чем С++ (я знаю людей которые отказались писать на С++, при этом были счастливы писать на C#). И с точки зрения того что он проще он значит и лучше чем С++, но только относительно простоты(которая кстати тоже сложное понятие). Тем не менее интегрально, так сказать, С++ все таки будет лучше, мне кажется.

JBFW
01.07.2026 19:50С ностальгией можно вспомнить С++ образца 199х годов, когда он отличался от С только возможностью создавать классы и некоторыми вольностями в синтаксисе (на что С компилятор ругался).
Он был удобным. А не вот этот вот зоопарк.
Зато это обьясняет, почему некоторые с пеной доказывают, что "Ардуино это не С++!!!!" - хотя там как раз тот самый С с классами.

ZirakZigil
01.07.2026 19:50Ничего не мешает и сейчас писать на "си с классами".

rsashka
01.07.2026 19:50В “С с классами” сейчас нет большого смысла, а вот сделать С++ с нормальным синтаксисом может быть и выстрелит.

trinxery
01.07.2026 19:50Carbon уже есть. Не взлетел.
Скрытый текст

Также нету в топ-50 в https://innovationgraph.github.com/global-metrics/programming-languages.

rsashka
01.07.2026 19:50Я же написал с нормальным синтаксисом :-) И у него в любом случае не было шансов заменить С++.

ZirakZigil
01.07.2026 19:50Что такое нормальный синтаксис? Примеры?

rsashka
01.07.2026 19:50понятный и естественный для пользователей исходного языка.

ZirakZigil
01.07.2026 19:50Мне понятен и естественен синтаксис плюсов. Вы, конечно, можете привести пример типа []()}[{())(*) (условно, просто для примера), который я не пойму, но это будет из разряда Buffalo buffalo Buffalo buffalo buffalo buffalo Buffalo buffalo.
Что я хочу сказать: из того, что можно придумать дьявольскую конструкцию не следует, что нужно переделывать синтаксис.

SamaRazor
01.07.2026 19:50Но почему-то получается так, что дьявольских конструкций на kloc кода в С++ обычно больше всего. Может конечно место (программисты) прокляты, но вольности которые язык позволяет явно этому способствуют.

Qwest_Prozto
01.07.2026 19:50Синтаксис C++ вполне понятный и естественный, но крайне сложный. А вот например синтаксис Python легкий, естественный но не всегда понятно, что происходит внутри

MountainGoat
01.07.2026 19:50В смысле "никто не мешает?" Открываешь чужой проект, к которому нужно пристыковаться, а там в коде больше двоеточий, чем было дырок на перфокарте.

redfox0
01.07.2026 19:50Немного про Rust, там легко выразить требования к типу. Нужно, что тип можно было сравнивать? Напиши так:
pub fn sort<T: Ord>(slice: &mut [T]) { todo!(); }
Johnny_Depp
01.07.2026 19:50static_cast - Преобразование указателей/ссылок в иерархии наследования вверх (к базовому классу) и вниз (к производному), но без проверок во время выполнения (вы должны быть уверены в типе сами). Вызов явных конструкторов или операторов преобразования.
dynamic_cast - Безопасное преобразование во время выполнения. Единственный каст, который работает во время выполнения программы. Если преобразовать указатель не получится - вернет nullptr. Если преобразовать ссылку не получится - выбросит исключение std::bad_cast. Работает только с полиморфными классами (у которых есть хотя бы одна virtual функция, обычно деструктор).
const_cast - Единственный каст, который умеет убирать (или добавлять) модификаторы const и volatile. Например, когда вы вызываете старую C-функцию, которая принимает char* (неконстантный), но вы уверены, что эта функция не будет менять данные, а у вас есть только const char*.
reinterpret_cast - Переинтерпретировать (прочитать заново) биты одного типа как биты другого типа. Компилятор просто берет адрес и считает его адресом другого типа, не генерируя никакого машинного кода для преобразования.
bit_cast - Появился в C++20 как альтернатива опасному reinterpret_cast. Он не переинтерпретирует указатели, а создает НОВЫЙ объект, полностью копируя биты старого в новый.
Зачем вы, не желая даже 5 минут погуглить, лезет со своим Rust? При чём тут он? Здесь другой язык, с более чем понятной и удобной логикой
redfox0
01.07.2026 19:50Это было к:
Компилятор физически не мог сказать «вы нарушили требование X», потому что никакого X нигде не записано и мог только дотащить вас за шиворот на строку 4212 в недрах и показать, что там для вашего типа не определён operator<, а чтобы вы поверили, то приходится выложить весь стек инстанцирования по дороге.

bromzh
01.07.2026 19:50с более чем понятной и удобной логикой
5 кастов, и 1 из них депрекейтед. Очень удобно и понятно, хороший язык

JBFW
01.07.2026 19:50pub fn sort<T: Ord>(slice: &mut [T]) {Хе-хе )
Есть (до сих пор) такой замечательный язык perl, который замечателен в том числе тем что можно написать так:
foreach my $var (@arr){ $var = $var + 2; }Даже если вы не знаете язык - можно догадаться что оно делает.
А можно так:for(@arr){$_+=2;} $_ += 2 for @arr; @arr = map { $_ + 2 } @arr;И вроде как другие варианты "писать быстрее и занимает меньше места", но когда у вас есть простыня подобных закорюк - разобраться в них смогут не только лишь все.
А это значит, что рандомный "новый программист" плюнет, и возьмет что ему попроще и попонятнее с детства, например, python.
И вот теперь мы видим вот то, процитированное, в Rust - тоже "коротко и понятно" /s
Как и приведенная конструкция в C++ для получения рандомного числа от 1 до 100.
Такое отпугивает...

MountainGoat
01.07.2026 19:50В первом варианте совершенно непонятно, а arr то при этом модифицируется или это в холостую шарашить. Только полагаться на здравый смысл, а это в нашем деле – плохая привычка.

nickolaym
01.07.2026 19:50Кто мешает то же самое на плюсах?
template<std::comparable T, size_t N> void sort(std::span<T,N> slice) { ..... } void sort(auto& slice) requires requires { // нам нужно, чтобы над разыменованными итераторами был оператор < *std::begin(slice) < *std::begin(slice); } { ..... }
Astrowalk
01.07.2026 19:50При виде конструкции "requires requires" разве не хочется выстрелить в монитор из наградного пистолета? Хорошо, что я бросил C++ задолго до появления этого шедевра...

ZirakZigil
01.07.2026 19:50Самое смешное что это by design, а не что-то, что "так сложилось", потому что какая-то новая фича наложилась на какую-то старую.

garwall
01.07.2026 19:50Замечательный язык, хочешь, пиши как на расте, хочешь, как на перле, хочешь, пиши как на си.
Понять бы, как писать на c++

ImagineTables
01.07.2026 19:50Касты сами по себе зло, поэтому их сделали уродливыми специально.
C++ это неподражаемая смесь прагматизма и астронавтики. Причём, бо́льшая часть прагматизма унаследована от Си, а астронавтику привнесли с крестиками. Я вообще не представляю, как без сишной кодебазы и фанбазы такой язык смог бы стать популярным.
Процитированное это яркий пример астронавтики. «Касты зло». А то мы сами не знаем! Покажите мне программиста, который бы не смог спроектировать приложение без кастов. Однако, C++ в реальности не летает в вакууме космоса, он держится на таких вещах, как, например, WinAPI. Вызовы WinAPI с их
WPARAMиLPARAMдостаточно уродливы сами по себе. Имея необходимость с ними работать, я УЖЕ достаточно наказан жизнью. Но нет! Давайте сделаем их ещё уродливее! Зачем? Чтобы меня стошнило? Или они хотят, чтобы это воспитательное уродство удержало меня от вызовов WinAPI? Но тогда, простите, зачем мне C++? Тогда я бы писал на Шарпе. То же самое касается Direct3D, например. (Вернее, касалось, когда я последний раз на нём писал, а было это давно — не знаю, как там сейчас). И вообще COM. Короче, касты это суровая необходимость кода общаться с реальным внешним миром, а его всеми силами изолируют.Смешнее этого только заявление Степанова, что он проектировал свою библиотеку так, чтобы поощрять программиста писать алгоритмы. Это как если бы вы шли в магазин за набором инструментов, а дома обнаруживали, что он состоит из заготовок отвёрток разной степени заточенности и шлифовального камня. Ну и что, что тебе надо было собрать мебель или разобрать телефон, и ты не можешь просто взять готовый инструмент для достаточно типовой задачи. Зато тебе будет удобно выточить себе любую отвёрточку!
Я всё жду, когда кто-нибудь добрый обернёт стандартную библиотеку во что-нибудь удобное. Чтобы вместе того же
mt3999… я даже название его не помню, хотя пишу его регулярно… ну, как пишу — копипащу из ДипСика… писатьrandom.GetRandomInteger(0, 100);. И плевать мне на несовместимость. Хорошее настроение важнее.
Daddy_Cool
01.07.2026 19:50Насчет прагматизма... )
Я не настоящий программист - я научный работник.
Мне нужна была программка чтобы что-то там сделать со строками в большом файле, я попросил DeepSeek сгенерить и чтоб именно на Си, не используя С++, потому что я понимал , что в ++овском тексте я не разберусь. И... программка заработала сразу как надо, без всяких правок! Возможно потому что Си как раз более примитивный/низкоуровневый...
ImagineTables
01.07.2026 19:50И… программка заработала сразу как надо
Не хочу вас расстраивать, но есть шанс, что так только кажется. Строки это объективно тёмная, запутанная материя (владение ими; правильный перебор; (не)фиксированность ширины; глифы, символы, кодепоинты и байты строки — в чём разница?; нули в качестве маркеров и хранение размеров; (не)обходимость +1 под буфер для каждой внешней функции; глубокое понимание Юникода; переполнения всех мастей и т.д. и т.п.), и написать на Си что-то стопроцентно правильно работающее со строками требует высочайших навыков. По крайней мере, я уже много лет гоняю строки на C/C++, но не рискнул бы поставить жизнь или безопасность на то, что у меня это получилось.

Daddy_Cool
01.07.2026 19:50Возможно ). Я дальше чуток подумал, и понял, что мне эта программа была вовсе не нужна. )

Kelbon
01.07.2026 19:50Я всё жду, когда кто-нибудь добрый обернёт стандартную библиотеку во что-нибудь удобное. Чтобы вместе того же
mt3999… я даже название его не помню, хотя пишу его регулярно… ну, как пишу — копипащу из ДипСика…а что мешает самому обернуть под свои задачи за строк 10? Это же абсолютно элементарно делается. И потому и не добавлено в стандарт, потому что у всех разное получится под своё

ImagineTables
01.07.2026 19:50Я всё жду, когда кто-нибудь добрый обернёт стандартную библиотеку
Обернёт стандартную библиотеку, а не одного лишь Мерсенна.

Turbo_Pascal_55
01.07.2026 19:50Зато я знаю, какой язык самый лучший :)

Siemargl
01.07.2026 19:50История рассудила.
Из всего паскалевского семейства наиболее популярным стал Go.
Даже если родство сходу неочевидно =)

Groramar
01.07.2026 19:50Pascal, Go, Ada все в топ 20 Тиоба с переменным успехом. Сам пишу на Делфе и буквально квадратными глазами смотрю на подобные статьи. Нет, и Паскали далеко неидеальные языки. Но если сравнить с, то буквально верх совершенства. Увы и очень жаль что именно они не выстрелили как Плюсы в свое время. Ну что же, будем далее поедать кактусы. Лично у меня к Плюсам только одна претензия: скорость компиляции. Два часа на проект? Нет, это точно не мое. Был небольшой практический опыт общения. И это еще надо сказать 'спасибо' реальной реализации компилятора. i7 все несколько часов был занят почти под завязку по CPU и 32 гига по памяти :)
К слову, с совместимостью вверх на Делфе всё прекрасно. Личный опыт: перенос двухмиллионострочного проекта на 7 мажорных версий комплятора вверх с буквально пятью правками.

Turbo_Pascal_55
01.07.2026 19:50Это сейчас компиляция тяжелая.
В 90-х Turbo Pascal 5.5 компилил мгновенно, в отличии от Turbo C++ 2.0, который тормозил нещадно.
А уж Watcom C был вообще ужасом.
Siemargl
01.07.2026 19:50Plain С компилируется тоже очень быстро. C++03 - тоже. Дальше - хуже.
А уж Watcom C был вообще ужасом.
Зато 32-битным и потому почти все игры делались с его использованием.

ganqqwerty
01.07.2026 19:50Хоть что-то в жизни сделал не зря - убежал с плюсов когда они стали слишком большими (в 2011м стандарте, точнее раньше, когда его обещали под именем 0x)

rbdr
01.07.2026 19:50Хых. Как раз-то на 17 ну очень много всего сделано.
11++ была и есть мощнейшей оживляющей струей. Жаль только что на 26ом как-то всё под- заглохло. Херб Саттер не может уже, что ли. До сих пор шло хорошо.

Johnny_Depp
01.07.2026 19:50Неплохо конечно автор развёл всех на холивар :DDD

eao197
01.07.2026 19:50У меня есть версия, что статья написана специально для того, чтобы устроить перепись
ниасиляторовфанатов старого доброго “Си с классами” и Qt головного мозга. По типу вот этого или вот этого комментариев.
dalerank Автор
01.07.2026 19:50да там тоже жизнь особо мёдом не была, как вспомню проекты начала нулевых, аж зубы болеть начинают. Немного лучше стало с приходом 11 стандарта, боле менее устаканилось в 17, или я просто стал спокойнее и перестал сильно агриться на новые фичи

eao197
01.07.2026 19:50Особенностью моей работы является то, что пока в одном проекте можно использовать фичи свежего стандарта, рядом обязательно будет проект, отстающий на один два стандарта. Например, сегодня работаешь на C++20, завтра вынужден откатываться до C++17 или C++14 (вот C++11 давненько не было, к счастью). И каждый раз сразу видно скольких хороших вещей тебя лишили. Это ощущалось всегда: и при переходе с C++20 на C++17, и с C++17 на C++14… Особенно при переходе с C++14 на C++98/03. Тут вообще как будто на другой язык переучиваться приходилось.
Тем удивительнее в 2026-ом слышать панегирики C++98/03. Даже не смотря на то, что дичи в современные стандарты добавляют изрядно.

event1
01.07.2026 19:50В целом с текстом согласен, но вот это:
Rust по дизайну на порядок лучше. Стандартный компилятор, стандартная сборка, стандартный пакетный менеджер, никаких заголовочных файлов, лучшие что я видел сообщения об ошибках, нормальные значения по умолчанию, отсутствие неявных преобразований, нормальный UTF-8, sum-типы, и безопасность памяти на этапе компиляции.
верно только потому, что расту не нужна совместимость с С и он ещё молодой. Соответственно, у него нет груза обратной совместимости и необходимости добавлять новые фичи, чтобы использовать возросшие на порядки возможности машин разработчиков.

redfox0
01.07.2026 19:50Могу сказать, что груз обратной совместимости у Rust есть, но там удобные инструменты для миграции кодовых баз на новые версии языка.
cargo fix --edition

VitalyZaborov
01.07.2026 19:50Большое спасибо за статью! Теперь, если меня спросят, почему я не пишу на C++ - мне будет куда ссылаться.

ganqqwerty
01.07.2026 19:50Мне кажется что сильные команды приходят к выделению собственных стандартов. Хочешь написать как у Саттера в книжке? У нас в проекте нельзя так писать, пиши проще, вот тебе список допустимых конструкций.

Rafaell0
01.07.2026 19:50Хрестоматийный пример сложной простоты это случайное число, и где нибудь в джаве или пайтоне вы просто напишете random.randint(1, 100) и пойдёте дальше кодить, но не здесь. Это слишком просто, чтобы быть правдой.
Я попытался это исправить типо-безопасной библиотекой.
А сам random_device стандарт разрешает делать детерминированным и на старом MinGW он годами выдавал одну и ту же последовательность при каждом запуске, то есть длинная корректная мантра в реальности и длинная и коррекная, но не везде работает, хоть rand() % 100 и короче и хотя бы работает.

Rafaell0
01.07.2026 19:50Вроде Как я это исправил с помощью mt19937_64 и uniq_distribution. При стресс-тестах на set и unordered_set повторов (во всех смыслах) отловить не удалось.

Medeyko
01.07.2026 19:50Упоминая Rust, почему-то не вспоминают, как там решена задача обеспечения обратной совместимости. А ведь в контексте этой статьи это как раз самое интересное!
Там используются так называемые "редакции": это не имеющие полной совместимости сверху вниз версии языка. Просто для программы указывается, на какой редакции языка она написана. Для отдельных компонентов можно указать другую редакцию. Поэтому устаревшие конструкции из языка выпиливаются или модернизируются, не ломая совместимость с махровым легаси.
Если программист пишет на современном Расте (Rust 2024), ему в подавляющем большинстве случаев не нужно знать, что там было в Rust 2015.
В целом, вполне себе возможная стратегия и для будущих вариантов C++, в общем-то, на мой взгляд...

legendasofizma
01.07.2026 19:50На практике C++ ровно так и используют. Трудовой коллектив договаривается до версии языка и кодстайла. Например, что пишут на C++20 и запрещают использовать то-нибудь из него (что-то слишком заумное) и заодно весь голимый си. На кодревью палкой по морде дают за странный код, и всего делов, все пишут на "нормальном" подмножестве. Про большинство ужасов из этой статьи разработчики и не слышали никогда и не услышат - всё это им не нужно и никак не мешает жить, этого в их жизни нет. Если это вырезать из языка и закрыть под флагом компиляции, никто и не заметит, что из языка пропала половина. Именно таким способом достигается тот факт, что перегруженный монструозный C++ никакой гирей для разработчиков не является, изучать всё это не обязывает и про это даже никто не слышал. Вы спросите - "как это не обязывает, вот достанется тебе старый проект...". Не достанется, на старый проект я не пойду просто. Точно такой же свободный выбор, как "не ходить в C++, а использовать Go/Rust". Страдают писатели компиляторов, но их боль никто не слышит, потому что их физически мало на планете.

Medeyko
01.07.2026 19:50Ну, по-моему, это всё же несколько другое.
Во-первых, "Бить по морде на кодревью" это не то же самое что "автоматически отклонять компилятором". Если нет автоматического отклонения, можно что-то пропустить. Ну, для этого, конечно, есть всякие линтеры и прочее, которые обеспечивают выполнение требований по стилю кода, но это менее эффективно, чем прямое отклонение компилятором.
Во-вторых, всем приходится изобретать свои "велосипеды", придумывая, какое подмножество языка разрешать, а какое запрещать. Это порождает фрагментацию экосистемы - и в смысле кода, и в смысле навыков сотрудников (устраивающийся на работу должен будет знать все варианты, которые могут прийти в голову собеседующему, например). Это же касается и использования внутренних библиотек, компенсирующих какие-то недостатки стандарта - про это в статье упомянуто, см. "
// антипаттерн: «мне надоело писать static_cast»"; локальные руководства по стилю тоже порождают подобное, в общем-то, хоть и в меньшей мере.В-третьих, эти локальные требования по стилю кода не решают вопрос неудачных названий и конструкций.
Так что опыт Rust с его редакциями, на мой взгляд, для C++ мог бы быть полезен.

groaner
01.07.2026 19:50Традиционная ссылка на самый подробный разбор недостатков C++ (правда, на английском): https://yosefk.com/c++fqa/

domix32
01.07.2026 19:50К множественных примеров инициализации ещё можно добавить варианты с дедукцией и без дедукции типов.
std::vector a{a.begin(), a.end()}; // получится vector<decltype(asd)::iterator> std::vector<int> b{a.begin(), a.end()}; // попытается скопировать интыplacement new тоже в списке не вижу
std::vector<std::byte> buffer(1024); MyClass* ptr = new (buffer.data()) MyClass(); ptr->~MyClass(); // звать деструктор надо ручкамиВ качестве отдельной категории можно ещё вспомнить thread local инициализацию.
еще есть скрытый
rvalue_cast, но о нем чуть ниже.core guidlelines добавляют ещё narrow_cast для сужения более длинных типов к более коротким:
narrow_cast<int32_t>(int64_t{}). Всё для демонстрации понимания намерения.

sergey_vasin
01.07.2026 19:50Забыли написать про главное отличие С++ от остальных вариантов. Он стандартизированный. А это часто является одним из обязательных требований к инструменту.
И это то. Чего нет у большинства альтернатив и убийц С++

bromzh
01.07.2026 19:50Он стандартизированный
Куча UB, про каждый надо знать, а то без ноги останешся.
Чего нет у большинства альтернатив и убийц С++

VladimirFarshatov
01.07.2026 19:50Спасибо за статью, прочел на одном дыхании, поностальгировал так что словил симптомы аллергии, чихал с пару минут. Надо было ещё пройтись по UB для знаковой и беззнаковой арифметики, а то Ардуинщики очень любят millis() через переполнения..

dalerank Автор
01.07.2026 19:50Это к Свиридкину и @Andrey2008 лучше я не силен в UB, в смысле написать могу, но с починкой хуже

Andrey2008
01.07.2026 19:50Да, добро пожаловать :) Путеводитель C++ программиста по неопределённому поведению: часть 2 из 11

Siemargl
01.07.2026 19:50Итого:
C++ худший язык программирования всех времён. Да, вот перечень его недостатков: 1, 2, 3, .....
C++ лучший язык программирования всех времён. Да, вот перечень его достоинств....

alan008
01.07.2026 19:50summon @antoshkka
Куда только смотрит комитет по стандартизации C++!!! <sarcasm. no offence :) >

Solmik
01.07.2026 19:50Так что да, язык ужасный, поэтому открывайте уже свою ужасную IDE, и продолжайте писать на худшем языке программирования.
Во дни сомнений, во дни тягостных раздумий о судьбах моей родины, — ты один мне поддержка и опора, о великий, могучий, правдивый и свободный… (И.Тургенев)

SpaceJumper
01.07.2026 19:50А как правильно подходить к изучению c++ новичку с таким количеством нюансов?

dalerank Автор
01.07.2026 19:50как правильно отметили хабровчане выше, 95% этих нюансов вам понадобится очень не скоро. Поэтому, просто пиши код

Solmik
01.07.2026 19:50Рекомендую поиграться с контроллером Arduino, для него используется упрощенная версия C++
К контроллеру можно подключать разные датчики и исполнительные устройства, то есть можно написать программу и сразу видно как она работает - например, измеряет температуру и выводит на дисплей.
Можно и без "железа", на эмуляторе всё делать - но это не так интересно :)
MaxAkaAltmer
Virtual static жалко нету.
NeoCode2
Еще было бы интересно non-virtual abstract, т .е.
void foo() = 0; // без virtual
типа требования к наследникам реализовать фунцию, но без рантайм-виртуальности а скорее для всякого шаблонного кода.
arteast
это решается концептом в месте применения, по аналогии с указанием ABC в виртуальном случае
nickolaym
Именно что в месте применения.
Если это CRTP, - то все проверки должны быть в месте вызова (например, внутри функций-членов), но не на уровне определения класса.
Можно обмазывать проверками - SFINAE, requires - шаблоны функций-членов. Потому что шаблоны будут инстанцированы уже после того, как финальный класс определён.
arteast
Если не CRTP, то писать
void func(const DerivedFromBase auto& instance);выглядит вполне хорошо. Ничем не хуже олдскульного виртуальногоvoid func(const AbstractBase& instance). И при ошибке современные компиляторы в ошибке разжуют, почему именно концепт не применим.Если CRTP, то да, надо закапывать в тело члена (в конструктор - мимо него не пройдешь, если это не класс-как-пространство имен чисто из статических членов). Не прям вот идеально, не
template<DerivedFrombase T>, но вполне себе чистенько. Благо, с современными компиляторыstatic_assert(DerivedFromBase<T>);тоже будет по полочкам разложен в ошибке.HiItsYuri
Дык концепт + crtp
nickolaym
Не получится.
Если в CRTP-базе сделать requires или static_assert на уровне класса, то при попытке унаследовать произойдёт довольно подлая штука. А именно, CRTP-шаблон параметризуется неполным типом! Вот так вот.
HiItsYuri
В методе после полного определения можно будет уже проверить с помощью require и static assert, не? Редко пишу на шаблонах, не оч понимаю какая разница где проверять.
nickolaym
Разница в том, что на уровне определения класса могут возникать неразрешимые зависимости.
Например, derived::foo доступна тогда, когда доступна base::bar. Но base::bar доступна тогда, когда доступна (или не доступна) T::foo, где T = derived.
Ну а внутри функции можно делать ассерты. То есть, функция будет доступна, но, например, приводить к ошибке компиляции.
Кстати сказать, для создания неразрешимых зависимостей необязательно даже наследование. Достаточно сослаться на себя самого. Парадокс брадобрея, причём хоть переборчивого (который бреет только тех, кто не бреет себя), хоть просто любопытного (который бреет всех, но интересуется на входе).
(Это плохая иллюстрация, - зависимость будет неразрешима и за пределами определения класса. Просто чот внезапно пришло в голову показать фокус).
Ну а попроще, более классически, - почему класс в месте определения класса неполный, -
HiItsYuri
Спасибо за объяснения!
nickolaym
Вводит и запрещает эту сигнатуру в этом скопе. В скопе наследников (или в другом неймспейсе) можно перегрузить и использовать оттуда.
ZirakZigil
Но это не будет требованием. Я не смогу писать Iface->foo, потому что нет гарантий, что у класса под интерфейсом этот foo есть. А если каждый раз нужно будет явно указывать derived, то начинает пропадать смысл интерфейса.
nickolaym
Если обращаешься через интерфейс, то каким волшебным образом там будет доступна наследничья реализация невиртуальной функции?
А для шаблонов - ну, концепты и констрейны в помощь. Хотя и с оговорками. Как я уже написал ранее, CRTP не дружит с констрейнами. https://godbolt.org/z/dnhqz51qb
ZirakZigil
Дружит, если через соседский огород ходить: https://godbolt.org/z/EczfKTY54.
Но у этого есть очевидная проблема.
nickolaym
Очень вычурный дизайн, когда один миксин чего-то хочет от другого миксина.
Брошу всё, стану проституткой.
domix32
можно бахнуть final и молиться, что компилятор соизволит сделать тип плоским.
dalerank Автор
блин, еще забыл про final и пачку проблем с ним
Gargoni
Ромбовидное наследование )))
ReadOnlySadUser
deduced this?