Неопределённое поведение (undefined behavior, UB) — ситуация, для которой стандарт языка не задаёт никаких требований к тому, что произойдёт. Не «результат зависит от платформы», а «дальше может быть что угодно».
UB — не способ получить случайное число и не особый режим арифметики. Это ошибка программы.
Не из лекции
Разделы про санитайзеры и про соседние понятия (unspecified, implementation-defined) в материалах занятия не разбирались. Остальное соответствует лонгриду, где UB отведён отдельный раздел.
Чем это отличается от «непредсказуемого результата»
Если программа содержит UB, компилятор вправе считать, что этой ситуации не бывает, — и оптимизировать исходя из этого. Поэтому последствия не ограничиваются странным значением в одной переменной: может измениться логика кода вокруг, исчезнуть проверка, а программа при этом успешно пройдёт тесты.
Практический вывод: UB нельзя проверить экспериментом. «У меня работает» — не аргумент; в другой сборке или с другим уровнем оптимизации поведение изменится.
Что вызывает UB чаще всего
На старте курса встречаются:
- чтение неинициализированной переменной — см. Инициализация переменных;
- переполнение знакового целого (у беззнаковых поведение, наоборот, определено — см. Беззнаковые типы);
- целочисленное деление на ноль — см. Целочисленное деление и остаток;
- деление минимального знакового значения на −1: результат не представим
в том же типе, поэтому проверки одного лишь
b != 0для произвольных знаковых данных недостаточно; - сдвиг на отрицательное число позиций или на количество, не меньшее ширины типа левого операнда после продвижения, — см. Побитовые операции;
- выход за границы массива;
static_cast<int>изdouble, значение которого не влезает в диапазонint.
Дальше добавятся разыменование нулевого или висячего указателя, обращение к освобождённой памяти, гонки данных.
Соседние понятия
Стандарт различает несколько степеней «неопределённости», и путать их не стоит:
- undefined — гарантий нет вообще;
- unspecified — из нескольких вариантов реализация выбирает любой, но перечень вариантов известен (например, порядок вычисления аргументов функции);
- implementation-defined — реализация выбирает и обязана задокументировать
свой выбор (например, размер
int).
Как его обнаружить
Обычный запуск g++ main.cpp -o main && ./main про UB ничего не скажет:
программа соберётся и что-нибудь напечатает. Когда результат странный или
нестабильный, помогают дополнительные флаги — см. Компиляция и сборка:
g++ -g -fsanitize=undefined,address main.cpp -o main && ./mainСанитайзер подставляет проверки во время выполнения и печатает файл и строку, где произошло нарушение. Такую сборку делают для поиска ошибки, а не постоянно: программа работает медленнее.
Часть проблем видна и раньше — по предупреждениям компилятора с -Wall -Wextra.
Частые заблуждения
- «UB — это когда программа падает». Падение — самый удачный исход; опаснее, когда она продолжает работать.
- «Я запустил, получилось −294967296, значит переполнение так и работает». Увиденное однажды число ничего не доказывает: при другой оптимизации или на другой платформе результат будет иным.
- «
Floating point exceptionпосле деления на ноль — это исключение C++». Нет: так конкретная среда сообщает о сбое. К дробным числам и к механизму исключений языка сообщение отношения не имеет. - «Раз тесты прошли, UB нет». Тесты проходят и на сломанном коде.
- «Отключу предупреждения, и станет чисто». Предупреждение — часто единственный признак UB, который вы увидите.
Углубление — L02
Ещё один источник UB — пустой цикл:
while (x);Это неопределённое поведение, если x не меняет своего результата и не имеет
побочных действий. То есть это не «честное зависание», на которое можно
рассчитывать. См. Циклы.
Углубление — L03
С указателями и динамической памятью список источников UB пополнился:
- чтение неинициализированного указателя и разыменование через него — см. Указатель;
- разыменование нулевого указателя: по стандарту по нулевому адресу ничего не лежит;
- чтение или запись через висячий указатель — как на
объект, чей блок закончился, так и на объект после
delete(use after free); deleteдля памяти, выделенной не черезnew, — см. Динамическая память; удаление одного объекта дважды;- разыменование указателя, полученного арифметикой на отдельной переменной, — см. Арифметика указателей;
- переполнение при умножении
intнаint, которое лечится приведением одного операнда кint64_tчерез static_cast.
Не из лекции
Use after free, выход за пределы блока и утечки ловит
-fsanitize=address: сообщенияheap-use-after-free,stack-use-after-scopeиLeakSanitizer: detected memory leaks(GCC 16.2).
Углубление — L04
Не из лекции
В лонгриде L04 слова «UB» нет, но с массивами связаны два источника:
deleteвместоdelete[]для памяти изnew[]— GCC 16.2 предупреждает-Wmismatched-new-delete, AddressSanitizer сообщаетalloc-dealloc-mismatch, см. Динамический массив;- выход за границы массива, уже упомянутый выше, включая обращение к
a[N]в массиве изNэлементов.Массив переменной длины
int a[n]— не UB, а расширение компилятора, которого нет в стандарте C++.
Углубление — L05
Новый источник UB: функция с не-void возвращаемым типом дошла до конца тела
без return.
float Max(float x, float y) {
if (x < y) {
return y;
}
} // UB if execution reaches this point
Max(10, 4);Результатом не будет ни ноль, ни «последнее вычисленное число». На лекции
программа упала с Illegal instruction, но и на падение рассчитывать нельзя.
Исключение одно — main: конец её тела равносилен return 0;. См.
Функция.
Ещё одно — висячая ссылка: возврат ссылки на локальную переменную, см. Висячий указатель.
Не из лекции
GCC 16.2 с
-Wall:control reaches end of non-void function [-Wreturn-type]; программа завершилась сигналом SIGILL (код 132) — GCC ставит в такое место инструкцию-ловушку.