Если бы Кардинал Ришелье был программистом, он бы сказал: «Дайте мне шесть строк кода, написанных рукой самого профессионального C-программиста в мире, и я найду в них лазейку для вызова неопределённого поведения.

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

C++ послужил нам сполна, но на дворе 2026 год, и современная рабочая среда явно отличается от среды 1985 (C++) или 1972 (С).

И я далеко не первый, кто об этом заговорил. Помню ещё с десять лет назад читал статью какого-то известного человека, в которой он утверждал, что использование C++ вполне обоснованно можно подвести под нарушение закона Сарбейнза-Оксли (SOX). И хотя с остальной его критикой я не был согласен (как и с тем, что он путал «its» и «it’s»), конкретно с этим пунктом я никогда не спорил.

Мало того, со временем я всё больше убеждался в его истинности. На деле в С для возникновения неопределённого поведения (undefined behaviour, UB) есть гораздо больше возможных причин, чем вы могли предполагать.

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

А ведь есть и другие, более тонкие и нелогичные.

Дело не в оптимизациях

Похоже, некоторые считают, что достаточно компилировать код без включения оптимизаций, и тогда UB не страшно. Они верят в какую-то изначальную враждебность компилятора, который только и думает — «Ага! Неопределённое поведение! Я могу делать всё, что захочу!» — и рассчитывают, что отключение оптимизаций его остановит.

Это не так.

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

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

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

UB повсюду

Я не стану пытаться перечислить все варианты неопределённого поведения, а просто аргументированно покажу, что оно повсюду. И если никому не под силу сделать всё идеально, то как вообще можно винить в этом программистов? Мой главный посыл в том, что ВЕСЬ нетривиальный код на С и C++ содержит в себе UB.

Обращение к объекту с неправильным выравниванием

Возьмём для примера такой код:

int foo(const int* p) {
   return *p;
}

Если эту функцию вызвать с неправильно выровненным указателем (вероятно, с адресом, кратным sizeof(int), но кто знает), то это UB (пункт 6.3.2.3 стандарта C23).

На Linux Alpha это иногда приводило к вызову аппаратного исключения, которое ядро обрабатывало программно, эмулируя нужное вам поведение. В других случаях ваша программа могла просто упасть по сигналу SIGBUS.

На архитектуре SPARC это гарантированно приводило к SIGBUS.

Естественно, на x86/amd64 (далее просто x86) такой код проблем не вызовет. Да что тут говорить, это даже наверняка будет операцией атомарного чтения. Архитектура x86 известна своей крайней снисходительностью к ошибкам синхронизации кэша.

Итак, здесь мы имеем три случая:

  • помощь со стороны ядра (для некоторых инструкций чтения на Alpha);

  • сбой (другие инструкции чтения на Alpha и SPARC);

  • всё в порядке (x86).

А что с ARM, RISC-V и другими архитектурами? Что насчёт будущих? В какой-нибудь будущей архитектуре могут даже появиться специальные регистры для указателей на int, которые вообще не заполняют младшие биты, потому что таких указателей существовать не может.

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

И всё потому, что компилятор не обязан генерировать инструкции ассемблера, работающие с не выровненными указателями. Ведь это UB.

Или вот другой вариант:

void set_it(std::atomic<int>* p) {
        p->store(123);
}
int get_it(std::atomic<int>* p) {
        return p->load();
}

Будет ли эта операция атомарной, если объект невыровнен? Му*— вопрос поставлен неверно. Это UB. (Хотя, да, на практике это вполне может обернуться проблемой с атомарностью).

*Прим. пер.: Иероглиф 無 (му) часто используется в дзэн-буддизме и обозначает отсутствие, небытие или пустоту. В текущем контексте он используется автором как образный способ указать на бессмысленность вопроса.

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

По правде говоря, оно возникло даже раньше.

Не нужно винить функцию foo() выше. Проблему вызвало не разыменовывание указателя — для её появления было достаточно простого создания указателя.

Вот пример:

bool parse_packet(const uint8_t* bytes) {
        const int* magic_intp = (const int*)bytes;   // UB!
        int magic_raw = foo(magic_intp);  // Возможен сбой на SPARC.
        int magic = ntohl(magic_raw); // Здесь всё в порядке.
        […]
}

Проблема в приведении, а не в foo().

Компилятор вполне может придумать для младших битов int* конкретное назначение, например, использовать их для сборки мусора или хранения тегов безопасности.

Применение isxdigit() к char

bool bar(char ch) {
        return isxdigit(ch);
}

isxdigit() — это простая функция, которая получает символ и возвращает 1, если это шестнадцатеричная цифра: 0–9 или a–f. Она также может принимать значение EOF. Так, хорошо. А какое у EOF значение? Из раздела 7.4p1 стандарта C23 мы знаем, что это int, и можем сделать вывод, что его нельзя представить в виде unsigned char.

Поэтому isxdigit() получает int, а не char. Но любое значение типа char помещается в int, так что здесь проблем быть не должно. Приведение из char в int корректно, и если верить разделу 6.3.1.3, то всё в порядке, так?

Нет. Если bar() вызвать со значением вне диапазона 0–127, а в вашей архитектуре char является знаковым (что согласно параграфу 20 раздела 6.2.5 стандарта C23 определяется реализацией), то целое число окажется отрицательным.

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

int isxdigit(int c) {
        if (c == EOF) {
                return false;
        }
        return some_array[c];
}

Приведение float к int

int milliseconds(float seconds) {
        int tmp = (int)(seconds * 1000.0); /* НЕПРАВИЛЬНО */
        return tmp + 1; /* тоже НЕПРАВИЛЬНО (знаковое переполнение — это UB) */
}

Когда конечное значение реального типа с плавающей запятой преобразуется в целочисленный тип […] Если целую часть значения нельзя представить целочисленным типом, поведение не определено.

— 6.3.1.4

Кроме того, ввиду отсутствия явных правил в стандарте, это UB также возникнет, если float окажется бесконечным или NaN.

Так как же сравнивать float с INT_MAX? Нужно ли приводить float к int? Нет, это вызовет то самое UB, которого мы хотим избежать. Тогда приводить INT_MAX к float? А откуда вы знаете, что его получится выразить точно? Что, если при приведении INT_MAX к float значение округлится так, что перестанет влезать в int, и тогда ваше сравнение потеряет всякий смысл?

Может, сработает следующий вариант? Так вы упустите некоторые реально большие значения, но вдруг это не страшно?

int milliseconds(float seconds) {
        const float ftmp = seconds * 1000.0f;
        if (!isfinite(ftmp)) {
                // Или другой способ обработки ошибки.
                return 0;
        }
        if ((float)(INT_MIN + 1000) > ftmp) {
                // Или другой способ обработки ошибки.
                return 0;
        }
        if ((float)(INT_MAX - 1000) < ftmp) {
                // Или другой способ обработки ошибки.
                return 0;
        }
        // Теперь преобразование безопасно.
        const int tmp = (int)ftmp;
        if (INT_MAX == tmp) {
                // Или другой способ обработки ошибки.
                return 0;
        }
        // Теперь можно безопасно прибавить единицу.
        return tmp + 1;
}

А ведь я всего лишь хотел превратить float в int. :-(

Уверен, существует много программ, код которых принимает значения в секундах и преобразует их в целочисленные миллисекунды просто через умножение и приведение.

Хранение объектов по нулевому адресу

Большинство программистов с этим не столкнутся, но я сомневаюсь, что существует отвечающий стандартам С способ поместить объект по нулевому адресу. Тем не менее такое вполне может потребоваться при написании ядра ОС и встраиваемых программ.

Согласно разделу 6.3.2.3, целочисленная константа нуль (которая преобразуется в указатель) и nullptr являются «константой нулевого указателя» (я буду называть её просто NULL). При этом стандарт C не указывает, что конкретный указатель NULL должен ссылаться именно на физический нулевой адрес, поскольку стандарт описывает абстрактную машину C, а не реальное аппаратное обеспечение».

Гарантируется лишь то, что при сравнении NULL с нулём вы получите равенство. Но вы и знать не знаете, как это происходит внутренне — возможно, этот нуль преобразуется в нативный NULL для данной платформы, которым вполне может оказаться 0xffff.

В стандарте также явно говорится, что разыменовывание нулевого указателя, независимо от его значения, ведёт к неопределённому поведению. Такой пример приводится в разделе 3.4.3.

Это также значит, что вы не можете рассчитывать на создание нулевого указателя с помощью memset(&ptr, 0, sizeof(ptr));. Нельзя инициализировать структуры таким образом и думать, что указатели в их полях окажутся нулевыми. Причём это уже касается большинства программистов.

К слову, в некоторых старых машинах использовались ненулевые указатели NULL.

Но предположим, что у вас современная машина, где NULL — это указатель на нулевой адрес, и у вас там реально хранится объект.

Опять же, в разделе 6.3.2.3 говорится, что NULL не равен «любому объекту или функции». Значит, это UB:

void (*func_ptr)() = NULL;
func_ptr();

Стандарт С говорит «там нет функции». Откуда вам знать — возможно, у компилятора в этом случае даже нет внутреннего способа выразить ваше намерение. Вы можете возразить: «Но он же просто сгенерирует инструкцию вызова по битовому шаблону из всех нулей? Других адекватных вариантов нет».

А что вообще значит «из всех нулей»? На 16-битной x86 это будет 0000:0000? Или CS:0000?

Переменные аргументы и типы (например, printf с %ld вместо %lld)

Здесь получаем UB:

execl("/bin/sh", "sh", "-c", "date", NULL);     /* НЕПРАВИЛЬНО */
execl("/bin/sh", "sh", "-c", "date", 0);     /* НЕПРАВИЛЬНО */

А вот здесь уже нет:

execl("/bin/sh", "sh", "-c", "date", (char*)NULL);

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

И здесь тоже UB:

uint64_t blah = 123;
printf("%ld\n", blah);  /* НЕПРАВИЛЬНО */

Должно быть так:

uint64_t blah = 123;
printf("%"PRIu64"\n", blah);

Так как же тогда выводить uid_t? Как вариант, вы можете привести его к uintmax_t и вывести с помощью PRIuMAX. Но уверены ли вы, что uid_t беззнаковый? Думаю, в худшем случае вы получите на выходе бессмысленное число вместо -1.

Деление на нуль — это UB

Уверен, вы и так это знали. Но учитывали ли вы сопутствующие аспекты безопасности? Нередко бывает, что знаменатель поступает из непроверенных источников.

И здесь ещё много других нюансов. Стандарт C23 содержит 283 вхождения слова «undefined», и это без учёта тех случаев, которые не задокументированы.

Бонус: реализация без UB

Никто не способен просчитывать правила целочисленного расширения при беглом просмотре кода. Никто!

Статья и так уже затянулась, поэтому ограничусь парой примеров:

unsigned char a = 0xff;
unsigned char b = 1;
unsigned char zero = 0;
bool overflowed = (a + b) == zero;
// overflowed примет значение 0, а не 1.

unsigned char a = 0x80;
uint64_t b = a << 24;     // Бонусное UB(?)
// Теперь b равна 18446744071562067968 (ffffffff80000000), а не 2147483648 (0x80000000).
// Даже при том, что все наши переменные беззнаковые.

LLM здесь справляются лучше нас

Покажите LLM ЛЮБОЙ код на C, попросите найти в нём UB, и она справится. Причём на сегодня окажется права почти в каждом случае.

Честно говоря, мне стало немного не по себе, когда ИИ корректно нашёл неопределённое поведение в моём коде, и тогда я решил таким же образом прощупать зрелую и скрупулёзно написанную OpenBSD. Я выбрал первый инструмент, какой пришёл мне на ум — find — и он выдал целую кучу предупреждений.

Я отправил мейнтейнерам патч для исправления выхода за границы объекта при записи (а также логической ошибки, не связанной с UB). Я не стал слать им патчи для всех случаев UB, которые встречались на каждом шагу, отчасти, потому что проект OpenBSD в прошлом неохотно реагировал на баг-репорты. К тому же, у меня было ощущение, что «на практике всё наверняка не так плохо», и если уж разработчики OpenBSD решат искоренить из кодовой базы всё UB, то это должен быть масштабный системный проект, а не кто-то вроде меня, выступающий посредником между LLM и мейнтейнерами.

Так что же нам делать?

Мы не можем просто взять и выбросить кодовые базы на С и C++. Но и оставлять их в таком глубоко уязвимом состоянии тоже не вариант.

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

Хотя эта идея тоже не нова и озарением не является.

Тем не менее в 2026 году написание кода на С или С++ без анализа на UB с помощью LLM уже следует расценивать как нарушение SOX, да и просто как банальную безответственность. Если уж разработчики OpenBSD более 30 лет не могут отыскать эти проблемы, то каковы шансы у всех остальных?

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

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

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


  1. linashop
    24.05.2026 09:19

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


  1. geher
    24.05.2026 09:19

    Они верят в какую-то изначальную враждебность компилятора, который только и думает — «Ага! Неопределённое поведение! Я могу делать всё, что захочу!» — и рассчитывают, что отключение оптимизаций его остановит.

    Это одна из самых масштабных проблем UB. Встретив неопределенное поведение компилятор совершает совершенную дурь, молча выбрасывая куски кода. Причем отключение оптимизации не всегда спасает от этой дури. И главное, что бесит, что код выбрасывается молча. А потом думай, почему программа работает не так. Да и если UB нет, эта молчаливая оптимизация с выбрасыванием неиспользуемого кода часто выбешивает. Ну выбрасываешь, скажи, какого лешего ты это делаешь. Часто это может указывать на ошибки в логике самой программы. А там, где есть уверенность, что выбрасывается только по делу, можно это уведомление и отключить.

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

    Тем не менее в 2026 году написание кода на С или С++ без анализа на UB с помощью LLM уже следует расценивать как нарушение SOX, да и просто как банальную безответственность.

    Скрытая реклама ЛММLLM?

    Кстати, увы, но означенные LLM не таки всегда находят UB, а если находят, то не всегда там, где они на самом деле есть.


    1. KanuTaH
      24.05.2026 09:19

      Скрытая реклама ЛММLLM?

      Какая же она "скрытая", вся статья, можно сказать, ради этой фразы и писалась :) Покупайте наших слонов. А про то, как дерьмо этих слонов выглядит в реальности - молчок-с.


    1. cpud47
      24.05.2026 09:19

      Увы, если бы мы умели диагностировать и печатать, что мы нашли уб — в уб не было бы надобности.

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

      Существенная часть выбрасываний "мёртвого кода" связана с тем, что этот код оставила после себя предыдущая оптимизация. Вы вряд ли захотите видеть все такие предупреждения. Но если всё же захотите, то есть optimization report, например беглый гуглинг говорит, что `clang -Rpass=dead-code` делает то, что Вы просили...

      А отслеживать почему код стал мёртвым: от оптимизации, либо он изначально был написан пользователем — сложно. Мы всё ещё не до конца справились с точным дебагинфо, а вышеупомянутый трекинг и того сложнее.

      Хуже того, большинство странных оптимизаций (в присутствии уб) происходит после инлайнинга — как раз то место, где происходит большое количество "хороших" оптимизаций. Условно:

      int foo(T* ptr) {
        int id = ptr->id;
        update(ptr);
        update(ptr);
        return id;
      }
      
      void update(T* ptr) {
        if (ptr == 0) panic("...");
        update_or_make_bad_things_if_null(ptr);
      }

      Поэтому увы, как решать сложившуюся проблему с уб — не очень понятно (ну кроме тестов/санитайзеров/стат анализа).


      1. VelocidadAbsurda
        24.05.2026 09:19

        Когда выбрасывается код, ещё понятно, почему. Но мне вот недавно из отсутствия проверки результата функции, в некоторых случаях возвращающей null, GCC молча вставил в ветвь с null инструкцию UND #FF - т.е. он прекрасно понимал, что в этой ветви код упадёт, и втихую сократил путь к падению вместо того, чтобы обругать меня. Выглядит как издевательство, особенно учитывая, что куда более безобидные прегрешения вроде неиспользованного параметра по-умолчанию вылазят в предупреждения. Видимо, потому, что их успели включить в стандарт ещё в адекватные времена.


        1. cpud47
          24.05.2026 09:19

          Ну, он не понимал. Может Вы уверены, что там никогда не будет нулл. И тогда он наоборот решил "помочь".

          Если хотите, чтобы компилятор ругался на необработанные нуллы, то может рассмотреть zig/rust?


  1. SilverTrouse
    24.05.2026 09:19

    Можно скомпилировать код с использованием с++26 и уже в коде будет меньше UB.


    1. slonopotamus
      24.05.2026 09:19

      Осталось где-то найти компилятор с поддержкой c++26. Потому что их не существует.


      1. j4niwzis
        24.05.2026 09:19

        Для начала бы надо найти компилятор любого C++ вообще, ведь ни один не соответствует стандарту точно.


  1. 100h
    24.05.2026 09:19

    Каким образом я могу обратиться к адресу 0, используя язык программирования Rust? Существуют ли на 100% безопасные и предсказуемые подходы?


    1. DarthVictor
      24.05.2026 09:19

      Каким образом я могу обратиться к адресу 0

      Абсолютному или относительному?


      1. 100h
        24.05.2026 09:19

        physical address 0x00


        1. cpud47
          24.05.2026 09:19

          Самый надёжный (и при этом понятный) вариант — объявить символ и привязать его к адресу через линкер-скрипт. Этот же подход, по-хорошему, стоит использовать и в Си.


    1. SunrisePaladin
      24.05.2026 09:19

      let raw_ptr = 0 as *mut u32; (Мутабельный указатель на адрес 0). А дальше через unsafe {} творишь чтение/запись по адресу


      1. cpud47
        24.05.2026 09:19

        Это требует, что 0 был exposed address. Это почти наверняка не так. А потому уб


  1. SIISII
    24.05.2026 09:19

    Как по мне, основная беда от UB связана не с тем, что, дескать, аппаратура как-то не так отработает и т.д. и т.п., а то, что компилятору позволено творить любую дичь, даже когда в реальности поведение будет определённым на любой сколько-нибудь "нормальной" платформе.

    Возьмём, например, целочисленный знаковый тип размером 2 байта. Как известно, все современные "нормальные" платформы (а существуют ли современные "ненормальные", я, честно говоря, не знаю) для представления целых чисел используют дополнительный код. Соответственно, имея 16 бит, мы имеем диапазон от -32768 до +32767. Прибавление единицы к максимальному положительному значению технически даёт максимальное отрицательное: +32767 + 1 = -32768. И здесь возможны ровно два технически обоснованных результата:

    • операция сложения молча выполняется и даёт указанный абсолютно предсказуемый и технически корректный результат;

    • фиксируется возникновение переполнения, что, в конечном итоге, приводит к исключению (неважно, выполняется ли это чисто аппаратными средствами -- скажем, мэйнфреймы IBM умеют ловить такое переполнение, если это разрешено соответствующим битом маски, а на IA-32 aka x86 надо явным образом анализировать флаг OF с помощью команды INTO или же условным переходом).

    На мой взгляд, подобные ситуации не должны объявляться как UB на уровне языка и компилятора, т.е. стандарт языка должен указывать, а компиляторы должны реализовывать генерацию кода, выполняющего требуемую операцию, не взирая на возможное (или даже гарантированно возникающее) переполнение -- они, разве что, должны выдать предупреждение, если видят, что переполнение возникнет. А ещё лучше иметь на уровне стандарта (и, естественно, конкретных компиляторов) некую прагму, которая позволяет стандартным образом включать и отключать контроль переполнения во время выполнения программы -- даже если такой контроль, как в случае с IA-32, влечёт за собой потенциальное снижение производительности из-за необходимости выполнения лишних команд. Тогда ситуация остаётся управляемой для программиста: он знает, где переполнение допустимо, а где -- нет, и может соответствующим образом настраивать поведение программы стандартным образом.

    То же самое касается и многих других случаев UB. Скажем, для невыровненных указателей тоже можно предусмотреть прагму, заставляющую включать код их проверки, причём даже на платформах, где технически невыровненные доступы разрешены и работают корректно (такая проверка упрощает отлов невыровненных указателей, которые станут проблемой на других платформах), и прагму, обязывающую компилятор сгенерировать код, корректно работающий с невыровненными указателями (для IA-32 эта прагма де-факто ничего делать не будет, а вот на ряде других платформ при её использовании будет формироваться другой, более громоздкий код -- скажем, побайтовая загрузка четырёхбайтовой величины с последующей "сборкой" в регистре процессора). Ну и т.д. и т.п. Но вместо этого, как мы знаем, UB трактуется просто как право компилятора творить любую дичь, в том числе выкидывать сомнительный для него код.


    1. rukhi7
      24.05.2026 09:19

      С UB везде и постоянно происходит подмена понятий, потому что никто не задумывается где это поведение не определено. А не определено оно в стандарте, а не в конкретном компиляторе для конкретной платформы! Конкретный компилятор на конкретной платформе пользуется возможностями машинного языка этой конкретной платформы. Стандарт пишется для того чтобы эти возможности ЛЮБОЙ аппаратной платформы использовались самым эффективным образом, именно поэтому стандарт избегает регламентировать поведение там где это может повлиять на эффективность кода для самых распространенных конструкций программирования.

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

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


      1. geher
        24.05.2026 09:19

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

        Разыменование неинициализированного указателя в интенсивно оперативную память использующей программе- это таки всегда UB, поскольку никогда не знаешь, к чему это приведет, ибо лотерея. Может оказаться вполне доступный адрес, а может исключение случиться по причине попытки доступа "не туда".


        1. SIISII
          24.05.2026 09:19

          Если он вообще не инициализирован, т.е. в него никогда не выполнялась запись -- то да (в той ячейке, где он лежит, может находиться мусор). Однако если он равен нулю или любому другому определённому числовому значению, то и поведение, с точки зрения стандарта и языка, должно быть определённым: обращение к памяти по заданному им адресу. К чему это приведёт -- не забота компилятора, за это должен отвечать программист. (И да, нужна бы стандартная прагма, включающая и отключающая контроль за нулевыми указателями).


          1. IUIUIUIUIUIUIUI
            24.05.2026 09:19

            Однако если он равен нулю или любому другому определённому числовому значению, то и поведение, с точки зрения стандарта и языка, должно быть определённым: обращение к памяти по заданному им адресу.

            Должно.

            А на практике — нифига. Use-after-free — UB (хотя адрес — вон он, только что валидный был, а между free и текущей инструкцией ничего в память не писалось, отвечаю!). Алиасинг — UB (хотя адрес — вон он, до сих пор валидный, просто в переменной другого типа).

            С pointer provenance уже тоже разобрались? Абсолютно классический пример:

            int x = 1, y = 2;
            int *p = &x + 1;
            int *q = &y;
            if (p == q) {
                *p = 10;  // что тута происходит???
            }

            Стандартсмены 25 лет (с DR-260, 2001-й год) даже не могут определиться, UB здесь или нет по стандарту сишки. Это уже не UB, это уже мета-UB. Фрактал UB.

            Это просто дно.


            1. rukhi7
              24.05.2026 09:19

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


              1. IUIUIUIUIUIUIUI
                24.05.2026 09:19

                мне кажется с таким же успехом можно жаловаться на то, что дифференциальное исчисление непонятно как применять на практике

                Общего с тем, что я пишу, только два слова: «на практике», причём чисто синтаксически.

                Поэтому вам таки кажется.


            1. vesper-bot
              24.05.2026 09:19

              Согласно статье, тут UB в строке 2 - указатель p оказывается невыровненным и всё, приехали.


              1. Cerberuser
                24.05.2026 09:19

                &p + 1 - это не “указатель на p плюс один байт”, а “указатель на p плюс смещение на один размер того значения, на которое указывает p”. Так что всё там выровнено.


                1. 8street
                  24.05.2026 09:19

                  Тогда непонятно в чем UB? В том что Y станет равен 10 или не станет? Ну если бы программист захотел приравнять Y к 10 то он бы так и сделал. Может это проверка на устройство стека в какой-то конкретной архитектуре.


                  1. Cerberuser
                    24.05.2026 09:19

                    Формально, по стандарту, - в том, что мы не можем использовать указатель на переменную, чтобы прочитать/записать по нему что-либо, кроме этой переменной. Фактически, в реальном поведении компиляторов, - в том, что указатель из просто адреса превращается в пару “адрес + источник”, и указатели с гарантированно разными источниками будут разными (и компилятор свернёт соответствующее сравнение в false), даже если их адрес совпадает.


                    1. rukhi7
                      24.05.2026 09:19

                      у меня был какой- то мелкий ПИК-контроллер и на нем память была постраничная, то есть при переключении страницы все указатели превращались в тыкву! И на нем был Си компилятор, который я успешно использовал. Но этот компилятор физически не мог соответствовать стандарту, но тем не менее это был язык Си и он работал как язык Си. Я к тому что стандарты это хорошо, но реальное программирование это маленько другое, чем просто соответствие стандарту.


                      1. IUIUIUIUIUIUIUI
                        24.05.2026 09:19

                        Я к тому что стандарты это хорошо, но реальное программирование это маленько другое, чем просто соответствие стандарту.

                        Это аргумент не в пользу C, если что.

                        И для протокола язык C определяется как то, что соответствует стандарту. Если реализация не соответствует стандарту, то это не язык C.


                  1. vesper-bot
                    24.05.2026 09:19

                    По идее, нет гарантии, что при компиляции данного кода переменные x и y будут расположены рядом, причем &y может отличаться от &x на большее число, чем 1; а также нет гарантии, что указатель p таки выровнен - sizeof(int) может быть и 4 при аппаратном требовании выравнивания в 8 байт, тогда *p+1 будет показывать в странное место.


                    1. IUIUIUIUIUIUIUI
                      24.05.2026 09:19

                      По идее, нет гарантии, что при компиляции данного кода переменные x и y будут расположены рядом

                      Поэтому, если присмотреться, то можно заметить там if, проверяющий ровно это.


        1. rukhi7
          24.05.2026 09:19

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


      1. Pand5461
        24.05.2026 09:19

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

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

        То, о чём вы говорите, - это в стандарте имеет отдельные названия неуточнённое поведение (unspecified behavior) и поведение, определяемое реализацией (implementation-defined behavior). К этим вещам обычно претензий не возникает.


      1. IUIUIUIUIUIUIUI
        24.05.2026 09:19

        Стандарт пишется для того чтобы эти возможности ЛЮБОЙ аппаратной платформы использовались самым эффективным образом, именно поэтому стандарт избегает регламентировать поведение там где это может повлиять на эффективность кода для самых распространенных конструкций программирования.

        Тогда это могло бы быть implementation-defined behavior, а не undefined behavior. Стандарт мог бы не регламентировать конкретное поведение, но требовать этой регламентации от каждой конкретной реализации.

        Олсо, это автоматически означает, что писать кроссплатформенный код с использованием этих возможностей ЛЮБЫХ аппаратных платформ нельзя. (впрочем, там квантор ∀ всплывает наверх, но я не знаю, как это написать по-русски)

        Такой вот портабельный язык Шрёдингера. Или, скорее, язык с принципом неопределённости Гейзенберга: можно либо портабельно, либо производительно, но не вместе. И вы ещё никогда не знаете, писал ли автор соседней либы, TU и функции портабельно или производительно, и вероятность ошибиться больше ħ/2 в естественных единицах.

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

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

        Более того×2, люди, страдающие от UB, пишут на сях и плюсах программы чуть сложнее хелловорлдов (в отличие от славящих сишку вы-просто-пишите-без-багов-а-с-багами-не-пишите и прочих кекспертов с опеннета и хабра обр. 2026), которые компилируются в чуть больше, чем несколько сотен строк ассемблерного листинга, поэтому «вы всегда можете проверить» нереализуемо на практике даже при фиксированном компиляторе и кодовой базе.

        Офигенное предложение, короче, но на практике от него ноль толка.

        Это будет просто платформо-зависимый код там где это UB по стандарту. Одна из фундаментальных задач программирования разделять и изолировать платформо-зависимый и платформо-НЕзависимый код, а не жаловаться на UB.

        Только язык может этому помогать (например, имея возможность маркировать тот или иной кусок кода, скажем, как unsafe, или там м… ммм… мона… тьфу), а может мешать (потому что поверх конкретного железа наворачивается ещё семантика абстрактной машины языка, за которой ты должен следить сам без гарантий компилятора — угадайте, про какой это язык?).

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

        Это ещё одно красиво звучащее, но совершенно неприменимое на практике кекспертное «мнение»: якобы стандарт оказывается полезным.

        На практике разработчики компиляторов на плюсах могут ломать поведение в последующих версиях произвольно и тихо (то есть, без ворнингов, ограничиваясь строчкой в ченджлоге — вы же читаете ченджлоги на clang и LLVM? читаете же? я это делаю каждый их релиз, наверняка все остальные программисты на плюсах тоже), покуда поломка подходит под какой-нибудь UB, и говорить «у нас лапки стандарт, идите на три буквы ISO/IEC 14882». Разработчики компиляторов дружественных к пользователю языков себе такого позволить не могут независимо от наличия стандартов.


        1. rukhi7
          24.05.2026 09:19

          Такой вот портабельный язык Шрёдингера. Или, скорее, язык с принципом неопределённости Гейзенберга: можно либо портабельно

          мир в основе своей состоит из противоречий. Когда мы доходим до уровня этих противоречий мы должны КОРРЕКТНО моделировать эти противоречия, в этом нет ничего удивительного и любой язык на этом уровне будет языком Шрёдингера. Очень точный термин вы предложили по моему.

          Например если мы программируем что-то по микросекундам мы должны корректно совмещать дискретное и непрерывное.

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


          1. IUIUIUIUIUIUIUI
            24.05.2026 09:19

            мир в основе своей состоит из противоречий.

            Нет. Это стандартная отмазка людей, которые не хотят строить цельную модель мира.

            Например если мы программируем что-то по микросекундам мы должны корректно совмещать дискретное и непрерывное.

            А противоречия здесь в чём?

            А указатели изначально противоречивы

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

            Но это не аксиома.


            1. rukhi7
              24.05.2026 09:19

              мир в основе своей состоит из противоречий.

              Нет. Это стандартная отмазка людей, которые не хотят строить цельную модель мира.

              Это вы Шрёдингеру и Гейзенбергу с Эйнштейном расскажите. От чего они по вашему отмазывались?


              1. IUIUIUIUIUIUIUI
                24.05.2026 09:19

                А противоречия у них в чём, из которых бы мир состоял?

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


    1. ruomserg
      24.05.2026 09:19

      Вот ровно поэтому я ворчу, что разработчики стандарта злоупотребляют UB. И это - влияние C++, где сделали последовательно несколько глупостей:

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

      • Построили все это на SFINAE (т.е. компилятор генерирует тучи версий класса/функции - и потом отбраковывает их потому что они не компилируются)

      • С ужасом посмотрели на размеры исполняемых файлов, и пошли давать права компилятору вырезать куски кода просто потому что он нашел способ как признать этот код ненужным. Последнее особенно опасно - потому что многие UB проявляются только при определенных данных. Но компилятор выкидывает куски кода - как только сможет доказать что при КАКОМ-ТО сочетании входных данных будет UB.

      Имхо, решение очень простое. Помимо UB - стандарт языка определяет такое понятие как "platform dependent". Это означает что в тонком случае - разработчик компилятора на определенной платформе ОБЯЗАН выбрать какое-то поведение, описать его - и далее ему следовать. При этом, на другой платформе - это поведение может быть другим. Но по-большому счету, "C" - используется в двух ипостасях:

      • Как язык программирования общего назначения - и тогда можно забыть про UB потому что вы даже не лезите в эту область (а еще лучше - возьмите Java/Kotlin и не сношайте себе и людям мозг!)

      • Как высокоуровневый ассемблер для конкретной платформы. И в этом случае, нас видимо не интересует 100% совместимость. Тот код, который я написал несколько лет назад для AVR - никогда не заработает на STM32. Не потому что у меня там UB, а потому что там куча всего еще прибита гвоздями к платформе. И невозможно простой перекомпиляцией это решить. А значит, дело компилятора - трудолюбиво производить код, а не заниматься широкой интерпретацией намерений программиста - ибо если человек что-то написал, наверное это зачем-то нужно. И должен быть наверное закрытый список оптимизаций которые компилятор может сам выполнять. И они должны быть достаточно тривиальными, чтобы не вызывать проблем. А если нужно что-то нетривиальное - ну что ж, значит придется оптимизировать как раньше - ручками в коде, вроде никто от этого не умирал...

      И еще - комитет по стандартизации C - ИМХО поставил себе цель, о котрой его никто не просил: создать такое подмножество языка, которое исполнялось бы с одинаковым результатом и примерно одной производительностью - на любой из существующих (или даже не существующих) платформ. С учетом зоопарка этих платформ - такое подмножество языка стремительно вырожается в ноль. Но по-пути убиваются подмножества языка (через объявление UB) - которые имели смысл для конкретных (и очень распространенных платформ). Те же signed/unsigned для платформы x86 - прекрасно определены, никакой неопределенности - просто генерируй команды процессора и будет счастье... Ну и кто мешал определить это как platform dependent, а не как UB ? Никто, кроме гордыни авторов стандарта...


      1. alkneu
        24.05.2026 09:19

        >Но компилятор выкидывает куски кода - как только сможет доказать что при КАКОМ-ТО сочетании входных данных будет UB

        Хммм, слишком пессимистично, тогда что-ли нельзя использовать никакие целочисленные знаковые арифметческие операции (+, -, *, /, %)?

        int a, b;
        a+b; // (a,b)==(INT_MAX, INT_MAX) --> UB
        a-b; // (a,b)==(INT_MIN, INT_MAX) --> UB
        a*b; // (a,b)==(INT_MAX, INT_MAX) --> UB
        a/b; // (a,b)==(1,0) --> UB
        a%b; // (a,b)==(1,0) --> UB


      1. cpud47
        24.05.2026 09:19

        Но компилятор выкидывает куски кода - как только сможет доказать что при КАКОМ-ТО сочетании входных данных будет UB.

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

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


      1. cpud47
        24.05.2026 09:19

        Имхо, решение очень простое. Помимо UB - стандарт языка определяет такое понятие как "platform dependent". Это означает что в тонком случае - разработчик компилятора на определенной платформе ОБЯЗАН выбрать какое-то поведение, описать его - и далее ему следовать. При этом, на другой платформе - это поведение может быть другим. Но по-большому счету, "C" - используется в двух ипостасях:

        Для этого же есть implementation-defined и unspecified behaviour. И там даже прописываются возможные варианты поведения, из которых компилятор может выбрать.


    1. Pand5461
      24.05.2026 09:19

      Предложение хорошее, но поезд компиляторостроения, похоже, слишком далеко уже ушёл.

      Вот именно для случая с целыми, что вы описали, недавно была статья: https://habr.com/ru/companies/pvs-studio/articles/276657/

      С учётом UB при переполнении знаковых целых компилятор имеет полное право для операций с короткими целыми использовать полноширинные регистры, поэтому +32767 + 1 может быть по факту +32768, потому что под капотом операция выполнилась в 32 или 64 битах. И, если судить по вышеуказанной статье, такое поведение компиляторов можно на деле встретить.


    1. bgnx
      24.05.2026 09:19

      Тему с UB я разложил ниже, но вы упомянули переполнения чисел то это иная проблема в C/C++ которая ортогональна UB. На самом деле ошибкой в дизайне языков C и C++ является ограниченность и неконсистетность числовых типов (для знаковых чисел переполнение это UB а для беззнаковых это wrap-around). Дело в том что числовые типы помимо размера и знаковости на самом деле бывают как минимум 4 типов

      1) когда переполнение вызывает эффект wrap-around 

      2) когда переполнение вызывает эффект saturation

      3) когда переполнение вызывает эффект паники (с помощью дополнительных рантайм-проверок) либо ошибки компиляции (если на этапе компиляции компилятору удалось статически проанализировать и доказать переполнение действительно произойдет используя data-flow/range-analysis/symbolic-execution/etc) 

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

      И если глубоко обдумать эти четыре типа чисел и возможные дефолты для какого-то нового языка то можно прийти к выводу что числа с типом 4 (которые в C/C++ по дефолту для знаковых типов) на самом деле не должны быть дефолтом в языке поскольку этот тип является вынужденным - разработчик на самом деле хочет рантайм-чеков и паники но из-за тормозов вынужден заниматься микрооптимизациями. Соотвественно если мы хотим сделать язык безопасным то правильнее было бы выбрать другие дефолты потому что далеко не всегда, а точнее в >95% случаев при написании приложений и серверов, судя по популярности других интерпретируемых языков, нам не нужны эти микрооптимизации и логично сделать язык с безопасностью по умолчанию и возможность оптимизации по запросу но только после профайлинга и индетификации проблемы (когда рантайм-чек переполнения становится боттлнеком)

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

      По итогу в контексте вышесказанного у C/C++ дважды неправильные дефолты для числовых типов и единственное что радует так это то что C/C++ здесь не одинок и в других языках тоже неправильные дефолты (когда по дефолту wrap-around)


  1. Andrey2008
    24.05.2026 09:19

    Тем, кто хочет ещё глубже занырнуть в UB - Путеводитель C++ программиста по неопределённому поведению :)


  1. d3d14
    24.05.2026 09:19

    Удивлен, что в статье не призвали переписать на Rust.


    1. Dhwtj
      24.05.2026 09:19

      В safe Rust UB по построению недостижимо


      1. d3d14
        24.05.2026 09:19

        Много где недостижимо. Многие интерпретируемые языки, например.


  1. tatapstar
    24.05.2026 09:19

    Почему для C и C++ постоянно упоминают компиляторы и их работу?

    На С и С++ не писал, но в других языках более высокоуровневых к компилятору (к его поведению) никогда не обращался. Как бы прихожу уже на все готовое


    1. JediPhilosopher
      24.05.2026 09:19

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

      Все остальные популярные языки интерпретируемые или с промежуточным байткодом. Среда выполнения там контролируется авторами языка, поэтому все эти проблемы с выравниваниями и тонкостями аппаратной начинки тут исключены. Можно смело прописывать в стандарте языка все что надо, а остальное - дело VM/интерпретатора на конкретной платформе. Да, может потеряться производительность, но для подобных языков это не так важно.

      Плюс это в C/C++ мире произошло разделение, есть несколько крупных вендоров компиляторов под разные платформы, не связанных друг с другом ничем кроме стандарта. Есть MSVC, есть GCC, есть кто там, CLang или еще кто-то, я уже не помню. И все они примерно равны. Вроде больше нигде такого нет, в Java, дотнете, питоне, джаваскрипте есть по одной популярной референсной реализации, а все остальные узкоспециализированные и малоиспользуемые. В таком случае гораздо проще все стандартизировать и не оставить пустых мест.

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


      1. tatapstar
        24.05.2026 09:19

        Спасибо за ответ


  1. mastan
    24.05.2026 09:19

    Обращение к объекту с неправильным выравниванием

    Возьмём для примера такой код:

    int foo(const int* p) {   return *p;}

    Так, а в чём в данном случае вина именно С/С++? Какой язык предотвратит невыровненный доступ к памяти?

    На Linux Alpha это иногда приводило к вызову аппаратного исключения, которое ядро обрабатывало программно, эмулируя нужное вам поведение. В других случаях ваша программа могла просто упасть по сигналу SIGBUS.

    Есть примеры куда более доступные чем эти древние оси и архитектуры. Вполне современные чипы Cortex-M0/M0+, встречающиеся например в некоторых контролерах STM32 в наши дни, делают HardFault на невыровненной памяти.

    Приведение float к int

    И это не имеет отношения к языку. Переполнения возможны на любом языке и задача программиста учитывать это. Я даже более страшную вещь скажу, банальное умножение int на int в 99% случаев двух взятых произвольных int'ов вызовет переполнение.

    Деление на нуль — это UB

    И это тоже универсально.

    Переменные аргументы и типы (например, printf с %ld вместо %lld)

    В плюсах есть libfmt/std::format, которые с синтаксисом похожим на printf проверяют аргументы на этапе компиляции.


    1. Cerberuser
      24.05.2026 09:19

      Какой язык предотвратит невыровненный доступ к памяти?

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

      Переполнения возможны на любом языке

      Но не в любом языке это undefined behavior. Где-то оно вполне может быть unspecified, где-то - определено однозначно.


    1. IUIUIUIUIUIUIUI
      24.05.2026 09:19

      Так, а в чём в данном случае вина именно С/С++? Какой язык предотвратит невыровненный доступ к памяти?

      В safe-подмножествах нормальных языков это сделать невозможно. А «C/C++» — там вся программа один большой unsafe, поэтому пристально смотреть нужно тоже на её всю целиком.

      Я даже более страшную вещь скажу, банальное умножение int на int в 99% случаев двух взятых произвольных int'ов вызовет переполнение.

      Вопрос в том, является ли это UB.

      И это тоже универсально.

      UB универсально?

      λ> 10 `div` 0
      *** Exception: divide by zero

      Но можно пойти дальше, в более другие языки!

      Main> 10 `safeDiv` 0
      Error: Can't find an implementation for IsSucc 0.
      
      (Interactive):1:1--1:15
       1 | 10 `safeDiv` 0
           ^^^^^^^^^^^^^^

      Функция деления требует доказательства, что делитель — не ноль. Такого доказательства она не находит (потому что доказательства, что ноль — не ноль, существует), поэтому код просто не компилируется.


      1. eao197
        24.05.2026 09:19

        Но можно пойти дальше, в более другие языки!

        И где же, где же безопасные ОС, браузеры, офисные пакеты и прочий полезный софт на этих более других языках?


        1. IUIUIUIUIUIUIUI
          24.05.2026 09:19

          В экономике. Цена ошибки в браузере, ОС или офисном пакете меньше цены разработки на верифицированном языке.

          Я так-то и обычные тесты не везде писал так-то, потому что не везде цена написания тестов покрывается их профитом.


          1. eao197
            24.05.2026 09:19

            Цена ошибки в браузере, ОС или офисном пакете меньше цены разработки на верифицированном языке.

            Погодите, погодите. Вы хотите сказать, что эти самые другие языки, на которые вы так запали, тупо не эффективны?


            1. IUIUIUIUIUIUIUI
              24.05.2026 09:19

              Настолько же, насколько плюсы «тупо неэффективны» потому, что для бекенда веб-сайтов чаще всего выбирают не C++.


              1. eao197
                24.05.2026 09:19

                Настолько же, насколько плюсы «тупо неэффективны» потому, что для бекенда веб-сайтов чаще всего выбирают не C++.

                Именно, поэтому вы вряд ли встретите C++ников, которые бегают по ресурсам для Web-разработки и доказывают, что специализированные решения на C++ рвут по производительности условные RoR и Django.


                1. IUIUIUIUIUIUIUI
                  24.05.2026 09:19

                  C++ рвут по производительности условные RoR и Django

                  Хм, C++-ники никогда не утверждали, что плюсы быстрее рубей или питона?

                  Или вы не это имели в виду?


                  1. eao197
                    24.05.2026 09:19

                    Или вы не это имели в виду?

                    Не это. Но у меня уже давно нет надежды объяснить вам что-то простое.


                    1. IUIUIUIUIUIUIUI
                      24.05.2026 09:19

                      Попробуйте объяснять не подменами — может получиться!


                      1. eao197
                        24.05.2026 09:19

                        Попробуйте объяснять не подменами

                        А вот это:

                        для бекенда веб-сайтов чаще всего выбирают не C++.

                        Хм, C+±ники никогда не утверждали, что плюсы быстрее рубей или питона?

                        Перевод темы с “бекенда веб-сайтов” на “плюсы быстрее рубей или питона” вообще еще “не подмена” или же уже “подмена”? Как вам зеркало подсказывает?

                        По сути же: я лично наблюдаю снижение интереса к C++ где-то года с 1998-го, может быть раньше. И если в 1990-х это было еще плавное снижение, то в 2000-х уже лавинообразное. Основной же причиной всего этого была как раз экономическая неэффективность разработки на C++ в сравнении с более безопасными мейнстримовыми языками. Тем не менее, прошло уже больше 20 лет с момента, когда начался массовый отток из C++ в Java/C# и пр. языки, а C++ все еще используется. И продолжает не смотря на то, что стабильному Rust- уже 11 лет исполнилось.

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

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


                      1. Cerberuser
                        24.05.2026 09:19

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

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


                      1. eao197
                        24.05.2026 09:19

                        Тут, наверное, основной вопрос - а есть ли ниши, в которых создание новых проектов на C++ будет оправдано?

                        Это хороший и объемный вопрос. Не готов его обсуждать в данной ветке.


                      1. rukhi7
                        24.05.2026 09:19

                        когда мы в браузере смотрим видео, это скорее всего загружаются и работают библиотеки (кодеки-плагины) написанные на С/С++ . Когда нужны определенные FPS кажется нет альтернативы. Ну и с DirectX я не знаю можно ли на чем то другом работать так же эффективно. И наверно можно долго перечислять всякие сервера и клиенты специальные типа SSH например. Интересно на чем написано VScode.

                        А еще я всегда вспоминаю свой восторг от Quake II в 2001 году еще! У нас весь офис зависал часами :) ! Какой фронт-енд! Какой бек-енд! Какая бизнес логика! Мне кажется это направление все также за С/С++ .


                      1. eao197
                        24.05.2026 09:19

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

                        Для C++ таких библиотек множество. Это только про чисто C++ны. Плюс огромное количество С-шных, которые из C++ используются как родные.

                        И если проект стартует на базе Boost+Folly+Abceil+ffmpeg+DearImGui и т.д., и т.п., то где грань между “новым” и “легаси”?

                        Поэтому и не хочется разводить рассуждения на эту тему.


                      1. rukhi7
                        24.05.2026 09:19

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


                      1. eao197
                        24.05.2026 09:19

                        Мне думается, что водораздел достаточно простой: у вас есть код проекта, в этом коде вы используете какие-то библиотеки напрямую. Вот это и имеет смысл рассматривать. А то, что библиотека где-то внутри как-то обращается к функциональности ОС – это уже за рамками разговора. Т.е. глубину рекурсии можно жестко ограничить.


                      1. IUIUIUIUIUIUIUI
                        24.05.2026 09:19

                        Перевод темы с “бекенда веб-сайтов” на “плюсы быстрее рубей или питона” вообще еще “не подмена” или же уже “подмена”?

                        Подмена, конечно. Я же даже N комментариями выше начал с «настолько же», потому что разговор выглядел так:
                        — Другой чувак: UB при делении на ноль универсально.
                        — Я: нет, вот пример.
                        — Вы: а чего тогда на этих языках не пишут ОС и браузеры?

                        Это вообще ничем не отличается по сути от гипотетического:
                        — Другой чувак: питон так же быстр, как C++.
                        — Вы: нет, вот пример.
                        — Я: а чего тогда на C++ не пишут бекенды?

                        Плюсы отбивают возможность видеть аналогии, или что?

                        И на этом фоне вы сравниваете C++

                        Где я в этой ветке сравнивал C++ хоть с чем-то?


                      1. eao197
                        24.05.2026 09:19

                        Подмена, конечно.

                        Тогда может не надо обвинять собеседника в собственных грехах? Вы делаете подмены, вы же от меня просите объяснить вам не прибегая к подменам.

                        Плюсы отбивают возможность видеть аналогии, или что?

                        Если вы хотите обсудить мою ненормальность, то вовсе необязательно приплетать плюсы. Я, собственно, на нормальность особенно и не претендую.

                        Где я в этой ветке сравнивал C++ хоть с чем-то?

                        Где-то здесь. Где вы заговорили про более другие языки.


                      1. IUIUIUIUIUIUIUI
                        24.05.2026 09:19

                        Тогда может не надо обвинять собеседника в собственных грехах? Вы делаете подмены, вы же от меня просите объяснить вам не прибегая к подменам.

                        Я вам показываю аналогией, как ваш «аргумент» выглядит в другом контексте. В каком месте это мой грех, если моя цель — перенести структуру вашего «аргумента» на, надеюсь, понятный вам контекст?

                        Но судя по тому, что некорректность вы таки увидели, половина дела сделана. Осталась вторая половина — чтобы вы увидели то же самое в ваших исходных словах.

                        Где-то здесь. Где вы заговорили про более другие языки.

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


                      1. eao197
                        24.05.2026 09:19

                        Я вам показываю аналогией, как ваш «аргумент» выглядит в другом контексте.

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

                        Это вообще ничем не отличается по сути от гипотетического:

                        — Другой чувак: питон так же быстр, как C++.

                        — Вы: нет, вот пример.

                        — Я: а чего тогда на C++ не пишут бекенды?

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

                        Осталась вторая половина — чтобы вы увидели то же самое в ваших исходных словах.

                        Вы упорно хотите заставить меня искать темную кошку в темной комнате. Только вот а) мне это не нужно и b) вряд ли эта кошка есть вообще.

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

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


                      1. IUIUIUIUIUIUIUI
                        24.05.2026 09:19

                        Здесь ничего и близко нет к тому, о чем я вам говорю.

                        А как называется переход от «язык позволяет выражать такие-то вещи [невозможность делить на ноль]» к «а чего тогда на нём не пишут $X, где эти вещи не являются определяющим фактором»?

                        Чем это отличается от перехода от «язык позволяет выражать такие-то вещи [более легко оптимизируемые программы]» к «а чего тогда на нём не пишут $Y, где эти вещи не являются определяющим фактором»?

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

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

                        Как там говорили великие? А, во:

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

                        Впрочем, да, если брать исходный комментарий не только в том месте, к которому вы «ответили», то да, сравнение C++ с другими языками там есть. Правда, на том же расте пишут и ОС, и браузеры. Сколько там нового кода в андроиде на плюсах, а сколько — на расте?


                      1. eao197
                        24.05.2026 09:19

                        А как называется переход от «язык позволяет выражать такие-то вещи [невозможность делить на ноль]» к «а чего тогда на нём не пишут $X, где эти вещи не являются определяющим фактором»?

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

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

                        Это я вам на языке “аналогий” пытаюсь донести то, что вы никак не поймете.

                        сравнение C++ с другими языками там есть

                        О том и речь.

                        Правда, на том же расте пишут и ОС, и браузеры.

                        Только вот ваши примеры относились не к Rust-у.

                        Сколько там нового кода в андроиде на плюсах, а сколько — на расте?

                        ХЗ, я не пишу код Андроида. В том, чем занимаюсь я в последнее время, Rust лично мне не кажется 100% оправданной заменой. Т.е. где-то за счет Rust-а было бы проще. Где-то – сильно нет.

                        То, что замены C++у появляются меня лично радует, т.к. не смотря на всю мою любовь к C++, тратить время на разбирательства с падениями из-за порчи памяти жалко. Пока, к сожалению, из адекватных замен только Rust.


                      1. IUIUIUIUIUIUIUI
                        24.05.2026 09:19

                        Это ключевой момент касающийся вашей критики C++.

                        Моя основная критика C++ — что это неюзабельный язык с фрактальной сложностью, единственная причина выбирать который для программирования в 2026-м — легаси.

                        Он не улучшает и не способен улучшить экономику новых проектов.

                        Этот поинт — это, вообще говоря, экономическое суждение.

                        Когда вы сравниваете C++ с Haskell-ем или Adga2, вы сравниваете огурцы с ананасами. И те, и другие можно есть, но они не являются друг другу заменой.

                        А это — другой поинт, и не надо его смешивать с предыдущим. Формально, я сравниваю не C++ с другими языками, а опровергаю утверждения с квантором всеобщности уровня «деление на ноль везде UB» †, или, в нашем с вами прошлом треде, что-то в духе «проверки на выход за границы массива всегда должны быть рантайм».

                        То, что тезисы вроде † в основном высказываются в контексте плюсов и подобных языков, не означает, что их опровержение обязательно сравнивает языки-контрпримеры с плюсами в сколь угодно осмысленном значении слова «сравнивать».

                        Этот поинт — это формально-логическое, а не экономическое суждение.

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

                        Ну вы же знаете, что будет дальше?

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

                        Это я вам на языке “аналогий” пытаюсь донести то, что вы никак не поймете.

                        Вы мне пытаетесь донести, что причины — [в том числе] экономические, и я по-вашему этого не понимаю, несмотря на то, что мой первый же ответ вам на эту тему начинался с «В экономике»?

                        Ну это уже несерьёзно.

                        Только вот ваши примеры относились не к Rust-у.

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

                        Топ стратегия.

                        ХЗ, я не пишу код Андроида.

                        Ну вот подсказка: в 2025-м вроде как были новости, что количество нового кода на расте превысило количество кода на плюсах.

                        тратить время на разбирательства с падениями из-за порчи памяти жалко

                        А могли бы позвать многочисленных комментаторов с хабра (и заодно с опеннета) — они знают, как надо, и пишут безбажный код по их словам!

                        Пока, к сожалению, из адекватных замен только Rust.

                        Смотря для чего. В моих задачах адекватная замена — DSL'и на хаскеле.


                      1. eao197
                        24.05.2026 09:19

                        Моя основная критика C++ — что это неюзабельный язык с фрактальной сложностью, единственная причина выбирать который для программирования в 2026-м — легаси.

                        Ну так не выбирайте. Вас кто-то силой заставляет?

                        А это — другой поинт, и не надо его смешивать с предыдущим.

                        Или это вам так кажется. Может вам хотелось бы, чтобы ваш комментарий, в котором вы приводите примеры на Haskell-е и Agda, воспринимался как многослойный с кучей поинтов и смыслов. Может вы даже пытались его таким сделать.

                        Но для одного конкретного читателя он выглядит гораздо проще: есть C/C++, которые сплошной unsafe, и есть более безопасные языки с парой конкретных примеров оных. И все.

                        Ну вы же знаете, что будет дальше?

                        Нет, я не умею предсказывать будущее.

                        Ровно это может быть использовано как «причина» для

                        Для каких-то выводов, которые странным образом рождаются в вашей голове и которые я не понимаю. Вы строите какие-то свои взаимосвязи и выдаете это за якобы следствия из моих слов.

                        Вы мне пытаетесь донести, что причины — [в том числе] экономические, и я по-вашему этого не понимаю, несмотря на то, что мой первый же ответ вам на эту тему начинался с «В экономике»?

                        Нет, я не это вам пытаюсь донести. Но, как уже было сказано выше, простые вещи вы не понимаете. Это доказано уже не первым разговором.

                        А могли бы позвать многочисленных комментаторов с хабра (и заодно с опеннета) — они знают, как надо, и пишут безбажный код по их словам!

                        Мне кажется, что гораздо проще и гораздо точнее будет просто вас охарактеризовать как умного идиота.


                      1. IUIUIUIUIUIUIUI
                        24.05.2026 09:19

                        Ну так не выбирайте. Вас кто-то силой заставляет?

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

                        Но для одного конкретного читателя он выглядит гораздо проще: есть C/C++, которые сплошной unsafe, и есть более безопасные языки с парой конкретных примеров оных. И все.

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

                        Нет, я не это вам пытаюсь донести.

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

                        я пытаюсь донести не это

                        Хм.

                        Мне кажется, что гораздо проще и гораздо точнее будет просто вас охарактеризовать как умного идиота.

                        Если у вас с аналогиями хуже, чем у идиота, то что это о вас говорит?


                      1. eao197
                        24.05.2026 09:19

                        Ну так и вы можете не сравнивать

                        Вас опять несет не в ту сторону. Мне, например, не нравятся такие языки, как Си, Java и Go. Но я не бегаю по Хабру и не доказываю, что есть языки лучше. И не страдаю от необходимости начинать новые проекты на них.

                        А вот вы страдаете. Ну так не страдайте.

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

                        Раз вы разобрались с нюансами C++ и освоили Haskell-и с Agda-ми, значит должны быть интеллектуально развиты. Но эта развитость удивительным образом не позволяет видеть простых вещей: вот есть ваш комментарий про более безопасные языки. Допустим, что читатель, который устал от проблем C++, смотрит на ваши примеры и хочет взять какой-либо из “более других” языков вместо C++. Ну, например, чтобы сделать свой Excel. Или сильно специализированную альтернативу nginx. И…

                        И в реальности он быстро обнаружит, что даже у Rust-а не будет безоговорочного преимущества. Не говоря уже про Haskell-и.

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

                        Если у вас с аналогиями хуже, чем у идиота, то что это о вас говорит?

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


                      1. IUIUIUIUIUIUIUI
                        24.05.2026 09:19

                        Но я не бегаю по Хабру и не доказываю, что есть языки лучше.

                        Да, вместо этого вы бегаете по хабру конкретно за мной и на каждое второе упоминание более других языков начинаете возражать non sequitur'ами. Зачем?

                        Моя неявная цель — чтобы чуть больше людей узнали про такую штуку, как зависимые типы, что они позволяют, и так далее, и смотрели на системы типов не как на врага народа (хотя после языков вроде джавы или сишки на них только так и смотришь), а на как друга, помощника и чуткую страховку. И это, сюрприз, помогает даже в плюсах!

                        А ваша неявная (или явная) цель какая?

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

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

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

                        Только вот каким бы ни был я сам, вы умным идиотом быть не перестанете.

                        Ничего страшного, аддитивной константой можно пренебречь.


                      1. eao197
                        24.05.2026 09:19

                        Да, вместо этого вы бегаете по хабру конкретно за мной и на каждое второе упоминание более других языков начинаете возражать

                        Возражать?

                        Хватит уже приписывать мне то, чего я не говорил.

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

                        Начать можно с того, чтобы сделать анализ а что из этих ваших “неоправданных предположений” были высказанны мной в реальности.


  1. Sobakaa
    24.05.2026 09:19

    Получается на ллм раз или в попу раст?


  1. MasterMentor
    24.05.2026 09:19

    Хабетс пишет: «В C всё является UB, компилятор сошел с ума». Но это ложь. Язык дал ему все инструменты безопасности, но он их игнорирует.

    Современные стандарты C (C23) и C++ давно дали легальные, безопасные инструменты. Вместо опасных кастов указателей, от которых Хабетс страдает в своем коде, есть std::span или встроенные макросы выравнивания. Вместо наивных проверок знакового переполнения есть функции безопасной арифметики (ckd_add, ckd_mul), встроенные в стандарт на уровне ключевых слов.

    – Концепция Профилей (Profiles): Сейчас в комитете по стандартизации ISO активно обсуждается внедрение так называемых «профилей безопасности» (Safety Profiles). Идея в том, чтобы программист мог прямо в коде написать #pragma safety или аналогичную директиву. Включая её, вы добровольно говорите компилятору: «В этом модуле UB запрещено, проверяй меня жестко».

    – Внедрение типов-оберток: Вместо изменения базовых типов вроде int или *pointer, комитет добавляет в стандарт безопасные альтернативы (например, std::span, функции безопасной арифметики). Хочешь безопасности - используй их, не хочешь - пиши по старинке на свой страх и риск.

    Undefined Behavior - это прямое воплощение закона формальной логики Ex falso sequitur quodlibet (Из лжи следует что угодно) в программировании.

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

    В серьезных отраслях (авиация, космонавтика, автомобильный софт) существуют жесткие стандарты программирования, такие как MISRA C или AUTOSAR. Там правило номер один полный, абсолютный запрет на использование любых конструкций, способных вызвать UB.

    Если программист пишет код для бортового компьютера самолета и оставляет там шанс для невыровненного указателя или знакового переполнения, надеясь, что «компилятор как-нибудь разберется» - этого человека с позором выгонят из индустрии. Это не просто низкая квалификация, это должностное преступление.

    https://github.com/sakura1083841400/MISRA-C/

    https://www.autosar.org/search

    Guidelines for the use of the C++14 language in critical and safety-related systems https://www.autosar.org/fileadmin/standards/R19-03/AP/AUTOSAR_RS_CPP14Guidelines.pdf

    UB (неопределенное поведение) в той или иной форме есть во всех языках программирования, за исключеним математических (Coq, Agda, итд). В управляемых языках это называют не UB, а «неожиданным / неочевидным поведением» (unexpected behavior).

    На Хабре есть десятки статей на эту тему (например, циклы статей «Занимательная Java» или «Тонкости и ловушки C#»). Вот пара «идиотских», но 100% законных примеров, от которых у практикующих программистов «дергается глаз»:

    ***
    1. Java: фокусы кэширования и автобоксинга
    В Java есть прекрасный «безопасный» механизм автоматического превращения примитивов в объекты (автобоксинг). Смотрим на официальный, компилируемый 
    
    Integer a = 127;
    Integer b = 127;
    System.out.println(a == b); // Выведет: true
    
    Integer x = 128;
    Integer y = 128;
    System.out.println(x == y); // Выведет: false!
    
    Почему это «законное безумие»? С точки зрения любого вменяемого человека, 128 == 128. Но согласно спецификации Java (JLS), объекты классов-оберток сравниваются по ссылкам (==), а не по значению.Однако, чтобы язык не тормозил, создатели Java встроили в рантайм кэш для чисел от -128 до 127. Для чисел в этом диапазоне Java возвращает один и тот же объект из кэша (поэтому 127 == 127 — это true, ссылки совпали). А для числа 128 создаются два разных объекта в памяти.Программист пишет абсолютно safe-код, тестирует его на маленьких числах — всё работает. Программа выходит в релиз, числа растут до 128, и логика приложения тихо, без ошибок ломается, потому что 128 != 128.
    
    2. C#: Коварная ленивость LINQ и замыкания
    Классический пример из C# (до версии C# 5.0 это была главная боль, но и сейчас на эти грабли наступают в других циклах).
    
    var actions = new List<Action>();
    
    for (int i = 0; i < 5; i++) {
        actions.Add(() => Console.WriteLine(i));
    }
    
    foreach (var action in actions) {
        action(); 
    }
    // Ожидание: 0, 1, 2, 3, 4
    // Реальность: 5, 5, 5, 5, 5!
    
    Код абсолютно безопасен (safe), никаких указателей. Но компилятор C# делает здесь законное замыкание (closure). Вместо того чтобы скопировать значение переменной i в лямбда-выражение на каждом шаге цикла, компилятор захватывает ссылку на саму переменную i. Когда цикл завершается, значение переменной i становится равным 5.
    И когда мы позже вызываем наши сохраненные функции, они все смотрят на одну и ту же ячейку памяти, где лежит 5. Для человека это идиотизм, а для компилятора C# — строгое выполнение стандарта.
    
    и так далее
    

    PS Сам Том - никогда не проектировал и не разработывал ни языки, ни компляторы, это типичный самоучка-“юниксоид” из 1990-х. Посмотрите на код Тома. Если так писать, - то “UB” точно не обберёшься. https://github.com/ThomasHabets/arping/blob/arping-2.x/src/arping.c


    1. PatientZero
      24.05.2026 09:19

      Зачем этот слоп на Хабре?


      1. MasterMentor
        24.05.2026 09:19

        А чем "слоп" отличается от знаний?
        Психоз вокруг С++ очень похож на психоз gotoненавистничества - Двухминуток ненависти (англ. Two Minutes Hate), когда не умеющие писать код граждане клеймили во всех бедах goto. Теперь козлом отпущения назначен Си.

        PS Я хотя бы код Хабетса открыл и посмотрел как он пишет. Типичный прикладник, без квалификации в области разработки языков или компиляторов.

        И вы, к примеру, в Яндекс отправьте на ревью и его код и эту феноменальную статью. Уверен, узнаете много интересного.


    1. MasterMentor
      24.05.2026 09:19

      И ещё интересно было бы узнать, что Том думает об этих флагах компилятора? ну, зачем они? что делают?

      -fsanitize=undefined
      -fsanitize=thread
      -fsanitize=memory
      -fsanitize=address
      


      1. IUIUIUIUIUIUIUI
        24.05.2026 09:19

        У меня не так давно была очень смешная бяка в корутинах (там вообще в плюсах очень легко выстрелить себе в ноги), где ни один из этих флагов не помог. Более того, без них падало, а с ними — нет.

        Ещё для работы с ними по-нормальному надо пересобирать с ними все зависимости, а это может быть больно.


        1. MasterMentor
          24.05.2026 09:19

          Подтверждаю, такое может быть. "Глюкавость" ненешних компиляторов, "сред" разработки и "инструментов" - тайна Полишинеля. Взять те же профайлеры: работающих днём с огнём не сыщешь (из мниатюрных - нашёл только этот Very Sleepy http://www.codersnotes.com/sleepy/ ). А ведь в 1990-е - были!
          Что до меня: в проектах только подмножество С++ (почти чистый С++98), POD структуры + подмножество STL, исключения - под запретом. Софты 20 лет без "падений" вообще. Поэтому аргументирую исходя из своего опыта. Я же не должен бежать за Томом, только из-за того, что у него иной жизненный опыт?


          1. IUIUIUIUIUIUIUI
            24.05.2026 09:19

            Что до меня: в проектах только подмножество С++ (почти чистый С++98), POD структуры + подмножество STL, исключения - под запретом. Софты 20 лет без "падений" вообще.

            Не верю, сорян.

            Почему-то безбажный код без падений и уязвимостей встречается в основном у комментаторов в интернете, и посмотреть и пощупать его никак нельзя. А там, где код пощупать можно, или где им пользуются на самом деле, с падениями, дырами и прочим всё прекрасно: если отфильтровать мой фид по слову «уязв», то там просто сходу dnsmasq exim telnet grub×21 gnupg glibc gstreamer…

            Достаточно открыть dnsmasq, и там уже хит-парад всего, за что мы так нежно, горячо и страстно любим сишку:

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

            переполнение буфера в функции extract_name(), позволяющее атакующему подставить фиктивные записи в кэш DNS и добиться перенаправления домена на другой IP-адрес.

            Уязвимость вызвана тем, что в функцию check_source() передавалась длина записи OPT вместо длины пакета, из-за чего функция всегда возвращала успешный результат проверки.

            чтение из области вне границы буфера при валидации DNSSEC

            чтение из области вне буфера в функции extract_addresses()

            Это одна (прописью: одна) новость в одном сраном демоне. Это не браузер и не ядро ОС по сложности. Это C-шит-бинго в коде на 35 тысяч строк:

            ~/Programming/readonly/dnsmasq (master) % cloc .
                 144 text files.
                 101 unique files.                                          
                  50 files ignored.
            
            github.com/AlDanial/cloc v 2.00  T=0.10 s (1008.7 files/s, 897657.3 lines/s)
            --------------------------------------------------------------------------------
            Language                      files          blank        comment           code
            --------------------------------------------------------------------------------
            C                                46           7171           4087          35065

            В других программах всё то же самое. Вот наваливающие базу деды© с glibc:

            Уязвимость вызвана некорректной проверкой границ внутренних буферов функцией iconv(), что может привести к переполнению буфера максимум на 4 байта.

            Exim, RCE:

            Из-за ошибки в коде бэкенда, несмотря на освобождение TLS-буфера обработчик BDAT продолжает читать данные из потока и вызывает функцию ungetc(), что при отправке следом незашифрованных данных приводит к записи одного байта в уже освобождённый буфер.

            Фряха, академический™ код, рут через execve:

            Проблема вызвана переполнением буфера в системном вызове execve

            RCE в gstreamer, 10 вулнов в одной новости, восемь из них — переполнение буфера переполнение буфера переполнение буфера переполнение буфера переполнение буфера переполнение буфера переполнение буфера, ещё две — целочисленное переполнение (потом наверняка тоже что-нибудь где-нибудь переполняется или не туда лезет):

            переполнения буфера в плагине rtp, возникающее из-за отсутствия должной проверки размера и данных при обработке полей X-QDM RTP

            переполнение буфера при обработке некорректных координат в субтитрах DVB-SUB

            переполнение буфера из-за некорректной проверки размера структур при разборе формата H.266 (VVC, Versatile Video Coding).

            переполнение буфера из-за отутствия должных проверок размера при разборе таблиц Хаффмана в изображениях JPEG.

            переполнение буфера из-за отсутствия должных проверок размера при разборе заголовков мультимедийных контейнеров в формате ASF.

            переполнение буфера из-за некорректной проверки структур при разборе мультимедийных контейнеров в формате RealMedia.

            целочисленное переполнение при разборе RIFF-блоков с палитрой в файлах AVI.

            целочисленное переполнение в реализвции кодека H.266 при разборе секций с изображениями.

            переполнение буфера в реализвции кодека H.266 при разборе APS-блоков.

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

            У меня, конечно, есть гипотеза (потому, что код у них не падает по принципу неуловимого Джо), но, может, я упускаю что-то?

            Подтверждаю, такое может быть. "Глюкавость" ненешних компиляторов

            gcc 2.95 — другое дело, понимаю. Вот в молодость-то мою компиляторы были безбажные, стандарт понимали как надо, код генерили как надо!

            Но вообще здесь нет глюкавости. Просто санитайзеры никогда не обещали ловить все баги. Любая инструментация меняет код и поэтому может менять его поведение.

            Взять те же профайлеры: работающих днём с огнём не сыщешь

            vtune? perf? Не, ерунда какая-то.

            из мниатюрных - нашёл только этот Very Sleepy http://www.codersnotes.com/sleepy/

            Very Sleepy is a free C/C++ CPU profiler for Windows systems.

            Windows

            А, сорян, вопросов больше не имею. В список к gcc 2.95 можете добавлять VC6 — тоже известный своей безбажностью инструмент.


            1. MasterMentor
              24.05.2026 09:19

              vtune? perf? Не, ерунда какая-то.

              А Вы сами-то vtune пользовались? какая его последняя версия? от какого года? какой размер дистрибутива этого профайлера сейчас? какое "подразделение" Intel его делало (подскажу: его писали русские, которые здесь же на Хабре кроют собственный код матом :) )?

              Полюбопытствуйте. А главное попробуйте поотлаживать на нём многопоточный код. Это невозможно. Он банально падает: раз через два.

              perf - а что, есть нормальные версии "perf" под Винду? И да, мне нужен профайлер по Windows. Или что? Windows уже запретили?

              Почему их множество не пересекается с множеством людей, пишущих опенсорс

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

              Так же предположу, что эти два множества не пересекаются исключительно в Вашей голове: дело в том, что критически важную часть всей open-source инфраструктуры (ядро Linux, Kubernetes, браузерные движки, облачные базы данных) создают и поддерживают инженеры на зарплате у корпораций (Google, Microsoft, AWS, Meta, Red Hat). И знаете, правильно настроенные сервера Linux и Винды работают без перезагрузки годами. Так что качественный "опенсорс, используемый на десятках миллионов машин" как раз и пишется теми людьми, которые по Вашим словам с ним "не пересекаются".

              *

              И не надо читать, то что написано на заборе. Opensource - это уже давно не некие "альтуристы", пишущие "за всё хорошее".

              Linux, Kubernetes, Node.js, Hyperledger, Prometheus. Выручка фонда ~$311 млн. Из них более $133 млн — прямые членские взносы и донаты компаний. Mozilla Foundation, Браузер Firefox, Thunderbird, движок Gecko. Выручка коммерческого крыла (Mozilla Corporation) превышает $500–600 млн в год.

              И так далее.

              PS В связи с тем что мы живём в разных мирах (Вы на полном серьёзе используете "vtune" и "perf", и в Вашем - нет Windows), и у нас нет и не может быть общих интересов, предлагаю и далее не пересекаться.


              1. IUIUIUIUIUIUIUI
                24.05.2026 09:19

                А Вы сами-то vtune пользовались?

                Пользовался, начиная с этак середины-конца нулевых, когда оно ещё называлось, собсна, vtune (и когда они бесплатные лицензии раздавали для некоммерческого использования), через все переименования во всякие там amplxe-gui, и заканчивая моей текущей работой вот в этом году.

                какое "подразделение" Intel его делало? Полюбопытствуйте.

                Что, неужто вы — из авторов?

                А главное попробуйте поотлаживать на нём многопоточный код. Это невозможно. Он банально падает: раз через два.

                УМВР, ЧЯДНТ? Отлаживаю профилирую и многопоточный код, и локфри-ерунду всякую, и прочее счастье.

                perf - а что, есть нормальные версии "perf" под Винду? И да, мне нужен профайлер по Windows. Или что? Windows уже запретили?

                Ну, сочувствую, что уж. Есть причина, по которой я не пользуюсь Windows даже для хобби-разработок дома: для, собсна, разработки она неюзабельна.

                Вот в squad там погонять вечерком — это хорошо, да.

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

                Если бы оно пересекалось в реальности, то такого количества дыр в опенсорсе бы у нас не было, а оно есть.

                Потому что…

                И не надо читать, то что написано на заборе. Opensource - это уже давно не некие "альтуристы", пишущие "за всё хорошее".

                …я нигде и никогда не говорил, что опенсорс — это альтруисты, и тем более не делал этого в предыдущем комментарии. Более того, я всегда над тезисом, что [юзабельный] опенсорс пишется альтруистами, смеюсь, и смеюсь очень громко.

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

                Олсо, вы:

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

                вы же прямо следующим абзацем:

                критически важную часть всей open-source инфраструктуры (ядро Linux, Kubernetes, браузерные движки, облачные базы данных) создают и поддерживают инженеры на зарплате у корпораций

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

                Шиза.

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

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


    1. IUIUIUIUIUIUIUI
      24.05.2026 09:19

      легальные, безопасные инструменты

      Ну давайте посмотрим на эти легальные, безопасные инструменты.

      Кстати, про эти легальные, безопасные инструменты даже взрослые дядьки пишут, вроде Шона Пэрента в конференциях про то, что std::optional поощряет небезопасные паттерны (operator* — unchecked), или другого шона, Бакстера в P3390R0 (который пишет то ж про std::expected , потому что создатели стандарта сделали ту же ошибку).

      Вместо опасных кастов указателей, от которых Хабетс страдает в своем коде, есть std::span

      Который может указывать вникуда.

      Кстати, непонятно, как это спасает от кастов. Как это спасает от кастов, а?

      или встроенные макросы выравнивания.

      Щито?

      Причём тут макросы (?) выравнивания (??) ?

      ckd_add

      Угу, «since C++26» (чё там, userver из соседнего треда уже на C++20-то перешёл?), и по удобству синтаксиса bool ckd_add(T *result, T l, T r) почти на уровне обычного плюсика (который на самом деле мог бы просто быть монадическим!)

      Концепция Профилей (Profiles): Сейчас в комитете по стандартизации ISO активно обсуждается

      активно обсуждается

      мемес.про.consideration.watch.png

      Внедрение типов-оберток: Вместо изменения базовых типов вроде int или *pointer, комитет добавляет в стандарт безопасные альтернативы (например, std::span, функции безопасной арифметики).

      Так типы-обёртки или bool ckd_add(T *result, T l, T r)? C++ — это не функциональный язык, здесь под типами-обёртками понимают обычные типы. Да и в функциональных языках, в принципе, тоже.

      UB (неопределенное поведение) в той или иной форме есть во всех языках программирования

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

      за исключеним математических (Coq, Agda, итд).

      Которые и надо использовать для систем, где надёжность важна.


  1. Newpson
    24.05.2026 09:19

    Деление на нуль — это UB

    только для целочисленного деления. В случае чисел с плавающей точкой существуют ±inf и NaN.


    1. SIISII
      24.05.2026 09:19

      Бесконечности и НаНы не везде существуют, если рассматривать всю совокупность имеющихся платформ.


    1. haqreu
      24.05.2026 09:19

      Или выброшенное исключение, причем программист обычно поведение контролировать не может


    1. TimurZhoraev
      24.05.2026 09:19

      всё зависит от реализации команды DIV, для x86 генерирует int 0 и не меняет регистры, ARM возвращает 0, DSP/MIPS иногда (!) дают 0 или 0xFFF... максимум. Для плавающей запятой ещё есть число EPS, которое можно инъецировать в любые сомнительные дроби чтобы можно было досчитать симуляцию с гигавольтами и тераамперами.


  1. TimurZhoraev
    24.05.2026 09:19

    "С" сейчас - это самый совершенный аппаратно-независимый ассемблер, включая диалекты OpenMP/CL/CUDA. Вот тебе память вот тебе калькулятор, далее управляй сам, флажками, агентами, счётчиками - это всё явно и более чем работает. Это вы ещё UB в Verilog/VHDL не заглядывали, особенно когда тайминги и прочие аппаратно-зависимые фишки лезут. Так что ручное, псевдоручное и вайб-управление памятью это то что нужно для bare metal проектов. На всё остальное - Питон.


  1. ImagineTables
    24.05.2026 09:19

    На Linux Alpha это иногда приводило к вызову аппаратного исключения […] На архитектуре SPARC это гарантированно приводило к SIGBUS.

    Ну и где они теперь?

    Кстати, SPARC делала Sun. Может, поэтому они и изобрели Java, где нет адресной арифметики?


  1. Dmitri-D
    24.05.2026 09:19

    Естественно, на x86/amd64 (далее просто x86) такой код проблем не вызовет. Да что тут говорить, это даже наверняка будет операцией атомарного чтения. Архитектура x86 известна своей крайней снисходительностью к ошибкам синхронизации кэша.


    Это не совсем так. В x86 есть SIMD инстукции которые требуют выравнивания и кидают аппаратное исключение, если операнд не выровнен. Вы можете поставить аттрибут что операнд выровнен и включить оптимизацию с использованием SIMD и вы вполне вероятно получите именно такие инсрукции в коде.
    Кроме того, начиная с 486 есть флаг AC в EFLAGS который будет кидать такие исключения по любому невыровненному операнду. Просто Линукс и Windows начали использовать платформу до 486го и посчитали это удобнее, или по причинам обратной совместимости оставили его в состоянии выключен.
    Про атомарность - тоже не будет если операнд в двух строках кеша.


    1. IUIUIUIUIUIUIUI
      24.05.2026 09:19

      В x86 есть SIMD инстукции которые требуют выравнивания и кидают аппаратное исключение, если операнд не выровнен.

      У подавляющего большинства SIMD-инструкций, требующих выравнивание до #GP, это самое выравнивание — ≥16 байт, что больше естественного выравнивания всех стандартных типов и их агрегатов (если явно не указывать alignas). Поэтому компилятору от (не)выровненности ни холодно, ни жарко — для среднего векторизуемого кода он не может ей воспользоваться. Да и разницы между теми же MOVAPS и MOVUPS нет для процессора, покуда они не переходят границы кэш-линии.

      Есть инструкции загрузки/выгрузки MXCSR, которые его на самом деле требуют (а иначе — #GP), но что они у вас делают в hot path?


      1. Cerberuser
        24.05.2026 09:19

        для среднего векторизуемого кода он не может ей воспользоваться

        По идее, если значения в массиве выровнены, допустим, на 8 байт, а SIMD требует 16, компилятор может сказать “проверим реальный адрес, если надо - обработаем первое значение отдельно, дальше гоним SIMD”, что будет невозможно, если значения не выровнены вообще. Но допускаю, что это уже не совсем “средний векторизуемый код”, да.


        1. IUIUIUIUIUIUIUI
          24.05.2026 09:19

          компилятор может сказать “проверим реальный адрес, если надо - обработаем первое значение отдельно, дальше гоним SIMD”

          Хороший поинт. Но, похоже, ему проще взять тот же MOVUPS и невыровненные аналоги, чем генерировать тяжёлый и потенциально ненужный обработчик головы и хвоста массива.

          Ну, может, специфика моих вещей, но в написанном мной коде (а я за соблюдением стандарта слежу почти ОКРно) компилятор такие обработчики не делает.


  1. JBFW
    24.05.2026 09:19

    Вайб "я хотел просто писать как на Бейсике - а приходится понимать как устроены регистры и знать сколько бит в int"

    Ну да, приходится. Просто нужно воспринимать С не как "быстрый Бейсик/Питон", а как "платформонезависимый ассемблер": он только по синтаксису платформонезависимый, а по железу есть нюансы.


    1. j4niwzis
      24.05.2026 09:19

      Да нет, сам C то совсем платформонезависимый. Просто не полагаться надо на детали конкретных платформ, а писать по стандарту.


      1. JBFW
        24.05.2026 09:19

        Язык - да. Код - нет.
        Вот то же выравнивание, например:

        typedef struct {
          unsigned char a;
          unsigned int b;
          unsigned int c;
        } s;

        Сколько байт в памяти оно займет?
        Что будет, если работать со структурой и ее элементами через указатели?
        Например, передать как (unsigned char *) и обратиться x[1] - это куда попадаем?

        А union с этой же структурой?

        Ковырял как-то программку одну, написанную для x86 под DOS, чтобы запустить ее на ARM64 - вот как раз потому что она исправно собиралась, но падала.
        Там как раз подобного много было - авторы явно не думали, что через много лет кому-то понадобится адаптировать это под совсем другую архитектуру.


        1. Cerberuser
          24.05.2026 09:19

          Что будет, если работать со структурой и ее элементами через указатели? Например, передать как (unsigned char *) и обратиться x[1] - это куда попадаем?

          В не определённое стандартом поведение. То есть, в то, что не считается корректным кодом на C.


        1. MasterMentor
          24.05.2026 09:19

          Абсолютно корректный код.

          Если это struct используйте его поля как `s.a, s.b, s.с` - и будет Вам счастье.

          В памяти "оно" займёт ровно sizeof(s) - будете спорить?

          "передать как (unsigned char *) " а почему её не передать как (double*)? или как (GodObject*)? Почему Вы тип `custom struct` вдруг решаете передать как char *? Где Вас этому учили?

          Какое отношение "авторы явно не думали" имеет к С? кто за них будет "думать"?

          Когда кто-то хочет сам работать с POD-структурами, пусть читает ABI. Как Вам такая идея?

          #pragma pack(push, 1)
          
          typedef struct {
            uint8_t a;
            uint8_t b;
            uint8_t c;
          } s;
          
          #pragma pack(pop)
          

          А тем кто не хочет/не способен разбираться в ABI может стоит просто придерживаться* стандартов С? (или найти более интересную, высокооплачиваемую и менее напряжённую работу).

          PS Кстати, сам встречал случаи когда переменые в коде автор не инициализировал, просто полагаясь на то, что конкретный компилятор устновит их в нуль. Но это ведь не обязанность компилятора, правда?

          В зависимости от опций компиляторы или присваивают не инициализированным контейнерам типа int нулевые значения, или нет. Несколько программ падали именно из-за этого. Установите правилом инициализировать переменные при их объявлении! https://habr.com/ru/articles/693660/


          1. JBFW
            24.05.2026 09:19

            Ну вот, а те кто там тогда писал - не видели абсолютно никакой проблемы в том чтобы объединить в один блок совершенно разные структуры, потому что во времена PC XT/AT её и не было: какая разница, char + int или 3 char подряд? Которые потом можно байт за байтом записать куда-то там.
            Работало же.

            Это к тому, что С в принципе работает достаточно близко к железу, это не Питон, о чем и речь. Глупо его в этом обвинять, или обвинять программистов что они не учли возможность существенного изменения железа, когда писали под конкретный девайс.


  1. CrashLogger
    24.05.2026 09:19

    В реальности мы как правило пишем код не под устаревшие или экзотические платформы, а всего под четыре: x86, x86-64, Arm, Arm64. Достаточно прогнать тесты на этих четырех случаях, убедиться, что все работает, и не раздувать проблему на ровном месте. И это уж точно не повод, чтобы переписывать всю кодовую базу на rust или что-там еще сейчас модно. Сколько уже появилось и сгинуло этих модных языков, а Си жил, жив и всех нас переживет.


  1. klirichek
    24.05.2026 09:19

    Вот такое дивное UB поймали полтора года назад. В функции FindEntry последний цикл превратился в “вечный”. При этом разглядывание данных показало, что нет, хранилище таблицы не заполнено, по логике цикл ДОЛЖЕН был закончиться.

    VALUE OpenHashTable_T<KEY,VALUE,HASHFUNC,ENTRY,STATE>::FindAndDelete ( KEY k )
    {
     ENTRY * pEntry = BASE::FindEntry(k);
     assert(pEntry);
    
     VALUE tRes = pEntry->m_Value;
     BASE::DeleteEntry(pEntry);
    
     return tRes;
    }
    …
    template <typename KEY, typename ENTRY, typename HASHFUNC, typename STATE>
    ENTRY * OpenHashTraits_T<KEY,ENTRY,HASHFUNC,STATE>::FindEntry ( KEY k ) const
    {
     if ( !m_iSize )
      return nullptr;
    
     int64_t iIndex = HASHFUNC::GetHash(k) & ( m_iSize-1 );
    
     while ( m_pHash[iIndex].IsUsed(*this) )
     {
      ENTRY & tEntry = m_pHash[iIndex];
      if ( tEntry.m_Key==k )
       return &tEntry;
    
      iIndex = ( iIndex+1 ) & ( m_iSize-1 );
     }
    
     return nullptr;
    }
    

    UB здесь возникло именно из-за совокупности двух функций. Обе функции в одном юните. Компилятор заинлайнил FindEntry внутрь FindAndDelete. А там логика такая: зовём FindEntry, получаем указатель на элемент, его разыменовываем… Если элемент не найден - FindEntry вернёт nullptr. Но ведь разыменовывать nullptr - это UB!

    И дальше пошла жара…

    assert там роли не играет (только больше запутывает). По факту - мы сразу разыменовываем указатель, возвращённый функцией. Значит - он НЕ МОЖЕТ быть nullptr (потому что это - ub. Можно разыменовать и упасть, а можно просто вообще устранить эту ветку. ub - значит, карт-бланш компилятору в этом случае!)

    Шаг номер два - раз nullptr нам разыменовать не дают - значит, FindEntry тоже никогда не возвращает nullptr (ну, зачем держать мёртвый код…). Поэтому компилятор внимательно выкинул все ветви, которые приводят к возврату nullptr.

    Самое критичное - он выкинул критерий завершения цикла. while ( m_pHash[iIndex].IsUsed(*this) ) превратилось в while ( true ) потому что иначе мы вылетим из цикла, а там return nullptr, а этого быть не может.

    Ну и сам “выстрел” - попытка удалить несуществующий элемент. Разыменования nullptr при этом не происходит, всё просто остаётся в вечном цикле…


    1. ruomserg
      24.05.2026 09:19

      Вот эта инверсия принципа причинности - на самом деле несколько бесит. Компилятор начинает творчески интерпретировать вызванный ранее метод на основании кода, который написан ПОСЛЕ этого вызова. Ну то есть понятно, что для компилятора эти "до/после" - являются абстрактными понятиями. Но вот для человеческого ума - распространие ограничений "обратным ходом" - и редактирование логики вызванного метода "задним числом" - это "массаракш! Тридцать три раза - массаракш!" (С) Стругацкие.

      Ну хорошо - разрешили компилятору делать что угодно в точке где допущено UB - хрен с ним, мы понимаем что не на всех платформах можно контролировать разыменование nullptr. Но зачем лезть от этого места дальше во все стороны - и радостно вычеркивать код под лозунгом "UB быть не может" ?!

      ИМХО вот это уже камень в огород создателей компиляторов - они имеют право не детектировать UB и продолжать исполнение без гарантий консистентности состояния программы (хотя писать по адресу 0 стоило бы - возможно платформа умеет ронять программу с диагностикой и без поддержки компилятора). Но вот накладывать на весь остальной код ограничения вытекающие из лозунга "UB быть не может" - как бы не стоило... Впрочем - мы же понимаем зачем это сделано. Иначе в obj-файле останется стопятьсот лишних специализаций шаблонов, которые мы в C++ сгенерировали из-за SFINAE и не можем найти повод выкинуть...


    1. MasterMentor
      24.05.2026 09:19

      Я просто счастлив, что установил стандартом не использовать nullptr - довольствуясь старым добрым NULL. :)
      После Вашего случая подумаю, может и от bool в пользу BOOL отказаться :).


  1. bgnx
    24.05.2026 09:19

    Похоже ни автор статьи ни все комментирующие не понимают сути проблемы с UB в C/C++. Что ж, попробую объяснить весь расклад. Смотрите, проблема не в оптимизациях комплиятора (я наборот считаю что они недостаточно агреcсивны) - проблема в том что

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

    3.65 [defns.undefined] undefined behavior behavior for which this document imposes no requirements [Note 1 to entry: Undefined behavior may be expected when this document omits any explicit definition of behavior or when a program uses an erroneous construct or erroneous data. Permissible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message). Many erroneous program constructs do not engender undefined behavior; they are required to be diagnosed. Evaluation of a constant expression (7.7) never exhibits behavior explicitly specified as undefined in Clause 4 through Clause 15. — end note]

    Перечитайте еще раз эту фразу

    Permissible undefined behavior ranges from ignoring the situation completely with unpredictable results, to behaving during translation or program execution in a documented manner characteristic of the environment (with or without the issuance of a diagnostic message), to terminating a translation or execution (with the issuance of a diagnostic message)

    и вдумайтесь - стандарт не обязывает от реализаций останавливать программу в случае возникновения UB, - он такой говорит - "ну вы типа можете выдать диагностику и остановить программу на этапе компиляции или в рантайме (и на том “спасибо”) но вообще-то чтобы получить лычку “standard-conforming” в случае возникновения UB вам как компилятору (который обеспечивает рантайм и отвественнен за выполнение) разрешается тихо проглотить это и продолжить выполнение в неконсистетном и непуправляемом состоянии с любыми возможными нарушениями". Разве это не дико? Я бы понял бы если бы стандарт разрешал бы подобную ситуацию в других режимах а но для получения лычки “standard-conforming” в обязательном порядке требовал бы от компилятора иметь опцию или режим в котором компилятор пусть ценой любого возможного замедления (путем вставки рантайм-проверок или вообще интерпретации) но все же гарантировал бы что в случае возникновения UB программа просто упадет но ни в коем случае не продолжит свое исполнение в неконсистеном режиме с разносом нарушающие все возможные инварианты

    В общем моя единственная претензия к стандарту С++ заключается всего лишь в том что он не обязывает имплементации стандарта иметь режим при котором программа в момент UB (пусть ценой любого возможного замедления) все же остановится а вполне себе разрешает имплементациям всегда комплировать или исполнять программу так что программа продолжает тихо выполняться в неконсистетном состоянии

    2) а потом компиляторы такие - “ага, ну раз разрешено в момент наступления UB делать что угодно, ну так давайте оптимизировать так как будто никаких UB нет” и это привело к ситуации что если UB происходит то программа действительно начинает выполняться в неконсистетном режиме из-за вырезанных if-ов и других инвариантов из кода и дальше я мог бы поискать список из несколько десятков статей на хабре с описанием этих ужасов которые происходят результате такой политики компиляторов, но мне лень :)

    При чем тут надо провести четкое разделение - я не против самих оптимизаций, я наборот считаю что компиляторы еще недостаточно агрессивно оптимизируют код. Я просто считаю что компиляторы вместо того чтобы здраво смотреть на вещи они приняли за мантру "если стандарт разрешает вместо аварийной остановки тихо выполнять программу в неконсистетном режиме, так давайте это делать даже не предоставляя иной опции” вместо того чтобы сказать, мол “знаете, мы не согласны с такой позицией и вот вам режим в котором компилятор будет гарантировать что в момент UB программа упадет но точно не продолжит свое выполнение в неконсистетном режиме”
    И дальше что касается оптимизаций, то смотрите какая интересная штука получается - как только включается этот режим то дальше компилятор встроив рантайм проверку (например проверка переполнения, проверка выхода за пределы массива и т.д с аварийным завершением работы и выдачей диагностики) может проводить самые агрессивные оптимизации которые только возможны и если он смог статически доказать (после whole-program анализа используя самые передовые data-flow, range-analysis, symbolic-execution и т.д техники) что заход в такой-то if невозможен то тем самым он сможет вполне безопасно вырезать эту рантайм проверку и тем самым ускорить программу.

    Ну а если юзер использует раздельную компиляцию и не хочет предоставлять все исходники всех зависимых библиотек или если они закрыты (но слава богу в 2026 году это редкость и можно вполне строить проекты используя библиотеки или зависимости которые имеют на 100% открытые исходники) или если юзер не хочет ждать несколько часов (но по идее инкрементальный режим с анализом на каждое нажатие клавиши и ввода кода должен значительно ускорить анализ) - ну тогда естественно возможностей доказать что-то статически у компилятора значительно поуменьшатся и юзер соотвественно получит замедление кода.

    Но главное это выбор, разве не так? Главное это возможность при необходимости таки обдумать трейд-оффы, понять чего мы лишаемся, понять чего нам стоит внедрить то или другое, может не так уж и сложно настроить whole-world сборку проекта полностью из исходников всех зависимостей (особенно если код этих зависимостей и так открыт) чтобы компилятор в момент статического анализа мог заглянуть в тело абсолютно любой вызываемой функции и т.д. Самое главное это выбор. А текущая политика компиляторов этого выбора юзеру не дает и не предоставляет режима при котором пусть с замедлением но все же будет гарантироваться остановка программы в момент UB вместо тихого продолжения выполнения в неконсистетном режиме


    1. ruomserg
      24.05.2026 09:19

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

      Но разработчики компиляторов стали пользоваться этим мандатом расширительно: "мы ЗНАЕМ что в программе не может быть UB, и можем оптимизировать ее на этом основании". Мандат "можно продолжать вычисления без диагностики и без гарантий консистентности" и "можно оптимизировать программу исходя из того что ситуации UB никогда не возникают" - это по-понятиям все-таки совсем не одно и то же! Хотя логически, да - второе может быть допустимым вариантом первого...


  1. IgorPie
    24.05.2026 09:19

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


  1. sergio_nsk
    24.05.2026 09:19

    Или вот другой вариант:

    void set_it(std::atomic<int>* p) {

    Статья про C внезапно перетекала в про C++.


  1. j4niwzis
    24.05.2026 09:19

    Тем не менее в 2026 году написание кода на С или С++ без анализа на UB с помощью LLM уже следует расценивать как нарушение SOX, да и просто как банальную безответственность. Если уж разработчики OpenBSD более 30 лет не могут отыскать эти проблемы, то каковы шансы у всех остальных?

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

    Это перевод, но всё же. Почему автор вообще считает, что LLM могут искать UB лучше людей вообще всех. Да, LLM оказались лучше конкретно этого человека, но это ведь не говорит о всех людях. На моей практике работы с LLM они пропускали UB (в C++ только), которые мне были видны сразу.

    Если "эксперт" не может найти UB в примерах этой статьи, то эксперт ли это?


    1. Seraphimt
      24.05.2026 09:19

      Почему автор вообще считает, что LLM могут искать UB лучше людей вообще всех

      Как минимум потому, что проанализировать миллионы строк кода большого проекта способна только нейронка? Возможно она не найдёт всё или даст false positive, но уж точно больше человека, как мне кажется.