Undefined behavior
Делает всю программу бессмысленной, если нарушаются определенные правила языка.
Содержание |
Объяснение
Стандарт C++ точно определяет наблюдаемое поведение каждой программы на C++, которая не относится к одному из следующих классов:
- ill-formed - Программа содержит синтаксические ошибки или диагностируемые семантические ошибки.
-
- Соответствующий компилятор C++ обязан выдать диагностическое сообщение, даже если он определяет языковое расширение, придающее значение такому коду (как в случае с массивами переменной длины).
- Текст стандарта использует термины shall , shall not , и ill-formed для обозначения этих требований.
- некорректная программа, диагностика не требуется - Программа содержит семантические ошибки, которые в общем случае могут быть не диагностируемыми (например, нарушения ODR или другие ошибки, обнаруживаемые только на этапе компоновки).
-
- Поведение не определено, если такая программа выполняется.
- implementation-defined behavior - Поведение программы варьируется между реализациями, и соответствующая реализация должна документировать эффекты каждого поведения.
-
- Например, тип std::size_t или количество битов в байте, или текст std::bad_alloc::what .
- Подмножеством определяемой реализацией поведения является локале-зависимое поведение , которое зависит от предоставленной реализацией локали .
- неопределённое поведение - Поведение программы варьируется между реализациями, и соответствующая реализация не обязана документировать эффекты каждого поведения.
-
- Например, order of evaluation , являются ли идентичные string literals различными, объем накладных расходов при выделении массива и т.д.
- Каждое неопределенное поведение приводит к одному из множества допустимых результатов.
|
(начиная с C++26) |
- неопределённое поведение - Нет никаких ограничений на поведение программы.
-
- Некоторые примеры неопределённого поведения: гонки данных, доступ к памяти за пределами границ массива, переполнение знакового целого числа, разыменование нулевого указателя, множественные модификации одного скаляра в выражении без промежуточных точек следования (до C++11) которые являются неупорядоченными (начиная с C++11) , доступ к объекту через указатель другого типа и т.д.
- Реализации не обязаны диагностировать неопределённое поведение (хотя многие простые случаи диагностируются), и скомпилированная программа не обязана выполнять какие-либо осмысленные действия.
|
(since C++11) |
НО и оптимизация
Поскольку корректные программы на C++ не содержат неопределённого поведения, компиляторы могут выдавать неожиданные результаты при компиляции программы с фактическим UB с включённой оптимизацией:
Например,
Переполнение знаковых чисел
int foo(int x) { return x + 1 > x; // либо истина, либо неопределённое поведение из-за переполнения знакового числа }
может быть скомпилирован как ( demo )
foo(int): mov eax, 1 ret
Выход за границы доступа
int table[4] = {}; bool exists_in_table(int v) { // возвращает true в одной из первых 4 итераций или неопределённое поведение из-за выхода за границы массива for (int i = 0; i <= 4; i++) if (table[i] == v) return true; return false; }
Может быть скомпилировано как ( демо )
exists_in_table(int): mov eax, 1 ret
Неинициализированная скалярная переменная
std::size_t f(int x) { std::size_t a; if (x) // либо x ненулевое, либо неопределённое поведение a = 42; return a; }
Может быть скомпилировано как ( demo )
f(int): mov eax, 42 ret
Показанный вывод был получен на более старой версии gcc
Возможный вывод:
p is true p is false
Недопустимый скаляр
int f() { bool b = true; unsigned char* p = reinterpret_cast<unsigned char*>(&b); *p = 10; // чтение из b теперь является неопределенным поведением return b == 0; }
Может быть скомпилировано как ( demo )
f(): mov eax, 11 ret
Разыменование нулевого указателя
В примерах демонстрируется чтение результата разыменования нулевого указателя.
int foo(int* p) { int x = *p; if (!p) return x; // Либо неопределенное поведение выше, либо эта ветка никогда не выполняется else return 0; } int bar() { int* p = nullptr; return *p; // Безусловное неопределенное поведение }
может быть скомпилирован как ( demo )
foo(int*): xor eax, eax ret bar(): ret
Доступ к указателю, переданному в std::realloc
Выберите clang, чтобы увидеть показанный вывод
#include <cstdlib> #include <iostream> int main() { int* p = (int*)std::malloc(sizeof(int)); int* q = (int*)std::realloc(p, sizeof(int)); *p = 1; // UB access to a pointer that was passed to realloc *q = 2; if (p == q) // UB access to a pointer that was passed to realloc std::cout << *p << *q << '\n'; }
Возможный вывод:
12
Бесконечный цикл без побочных эффектов
Выберите clang или последнюю версию gcc, чтобы увидеть показанный вывод.
#include <iostream> bool fermat() { const int max_value = 1000; // Non-trivial infinite loop with no side effects is UB for (int a = 1, b = 1, c = 1; true; ) { if (((a * a * a) == ((b * b * b) + (c * c * c)))) return true; // disproved :() a++; if (a > max_value) { a = 1; b++; } if (b > max_value) { b = 1; c++; } if (c > max_value) c = 1; } return false; // not disproved } int main() { std::cout << "Fermat's Last Theorem "; fermat() ? std::cout << "has been disproved!\n" : std::cout << "has not been disproved.\n"; }
Возможный вывод:
Fermat's Last Theorem has been disproved!
Некорректная конструкция с диагностическим сообщением
Обратите внимание, что компиляторам разрешено расширять язык способами, которые придают смысл некорректным программам. Единственное требование стандарта C++ в таких случаях — выдать диагностическое сообщение (предупреждение компилятора), если только программа не была "некорректной без требования диагностики".
Например, если расширения языка не отключены с помощью
--pedantic-errors
, GCC скомпилирует следующий пример
только с предупреждением
, хотя он
приводится в стандарте C++
как пример "ошибки" (см. также
GCC Bugzilla #55783
)
#include <iostream> // Пример изменения, не используйте константу double a{1.0}; // Стандарт C++23, §9.4.5 List-initialization [dcl.init.list], Пример #6: struct S { // нет конструкторов с initializer-list S(int, double, double); // #1 S(); // #2 // ... }; S s1 = {1, 2, 3.0}; // OK, вызывает #1 S s2{a, 2, 3}; // ошибка: сужение типа S s3{}; // OK, вызывает #2 // — конец примера] S::S(int, double, double) {} S::S() {} int main() { std::cout << "All checks have passed.\n"; }
Возможный вывод:
main.cpp:17:6: error: type 'double' cannot be narrowed to 'int' in initializer ⮠
list [-Wc++11-narrowing]
S s2{a, 2, 3}; // error: narrowing
^
main.cpp:17:6: note: insert an explicit cast to silence this issue
S s2{a, 2, 3}; // error: narrowing
^
static_cast<int>( )
1 error generated.
Ссылки
| Расширенное содержимое |
|---|
|
Смотрите также
[[
assume
(
expression
)]]
(C++23)
|
указывает, что
выражение
всегда будет вычисляться в
true
в заданной точке
(спецификатор атрибута) |
[[
indeterminate
]]
(C++26)
|
указывает, что объект имеет неопределённое значение, если он не инициализирован
(спецификатор атрибута) |
|
(C++23)
|
отмечает недостижимую точку выполнения
(функция) |
|
Документация C
для
Неопределённое поведение
|
|