Неопределённое поведение
Делает всю программу бессмысленной, если нарушены определённые правила языка.
Пояснение
Стандарт C++ точно определяет наблюдаемое поведение каждой программы на C++, которая не относится к одному из следующих классов:
- некорректная программа - В программе есть синтаксические ошибки или диагностируемые семантические ошибки.
- Соответствующий стандарту компилятор C++ обязан выдать диагностическое сообщение, даже если он определяет расширение языка, придающее смысл такому коду (например, массивам переменной длины).
- В тексте стандарта для обозначения этих требований используются слова shall, shall not и ill-formed.
- некорректная программа, диагностическое сообщение не требуется - В программе есть семантические ошибки, которые в общем случае могут быть недиагностируемыми (например, нарушения ODR или другие ошибки, обнаруживаемые только во время компоновки).
- Если такая программа выполняется, поведение является неопределённым.
- поведение, определяемое реализацией - Поведение программы различается в разных реализациях, и соответствующая стандарту реализация должна документировать последствия каждого варианта поведения.
- Например, тип
std::size_t, количество битов в байте или текстstd::bad_alloc::what. - Подмножество поведения, определяемого реализацией, составляет поведение, зависящее от локали, которое зависит от предоставленной реализацией локали.
- неуточнённое поведение - Поведение программы различается в разных реализациях, и соответствующая стандарту реализация не обязана документировать последствия каждого варианта поведения.
- Например, порядок вычисления, являются ли идентичные строковые литералы различными, объём накладных расходов на выделение памяти для массивов и т. д.
- Каждый вариант неуточнённого поведения приводит к одному из набора допустимых результатов.
| (начиная с C++26) |
- неопределённое поведение - Поведение программы ничем не ограничено.
- Некоторые примеры неопределённого поведения: гонки данных, доступ к памяти за пределами массива, переполнение знакового целого числа, разыменование нулевого указателя, более одной модификации одного и того же скалярного объекта в выражении без промежуточной точки следования(до C++11)без упорядочения(начиная с C++11), доступ к объекту через указатель другого типа и т. д.
- Реализации не обязаны диагностировать неопределённое поведение (хотя многие простые случаи диагностируются), а скомпилированная программа не обязана делать что-либо осмысленное.
| (начиная с C++11) |
Неопределённое поведение и оптимизация
Поскольку корректные программы на C++ не содержат неопределённого поведения, компиляторы могут выдавать неожиданные результаты, если программа с неопределённым поведением компилируется с включённой оптимизацией:
Например,
Переполнение знакового целого числа
int foo(int x)
{
return x + 1 > x; // either true or UB due to signed overflow
}
может быть скомпилировано как (демонстрация)
foo(int):
mov eax, 1
ret
Доступ за пределами границ
int table[4] = {};
bool exists_in_table(int v)
{
// return true in one of the first 4 iterations or UB due to out-of-bounds access
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) // either x nonzero or UB
a = 42;
return a;
}
Может быть скомпилировано как (демонстрация)
f(int):
mov eax, 42
ret
Показанный вывод наблюдался в более старой версии gcc
#include <cstdio>
int main()
{
bool p; // uninitialized local variable
if (p) // UB access to uninitialized scalar
std::puts("p is true");
if (!p) // UB access to uninitialized scalar
std::puts("p is false");
}
Возможный вывод:
p is true p is false
Недопустимое скалярное значение
int f()
{
bool b = true;
unsigned char* p = reinterpret_cast<unsigned char*>(&b);
*p = 10;
// reading from b is now UB
return b == 0;
}
Может быть скомпилировано как (демонстрация)
f():
mov eax, 11
ret
Разыменование нулевого указателя
В примерах показано чтение результата разыменования нулевого указателя.
int foo(int* p)
{
int x = *p;
if (!p)
return x; // Either UB above or this branch is never taken
else
return 0;
}
int bar()
{
int* p = nullptr;
return *p; // Unconditional UB
}
может быть скомпилировано как (демонстрация)
foo(int*):
xor eax, eax
ret
bar():
ret
Доступ к указателю, переданному в std::realloc
#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
Бесконечный цикл без побочных эффектов
#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>
// Example tweak, do not use constant
double a{1.0};
// C++23 standard, §9.4.5 List-initialization [dcl.init.list], Example #6:
struct S
{
// no initializer-list constructors
S(int, double, double); // #1
S(); // #2
// ...
};
S s1 = {1, 2, 3.0}; // OK, invoke #1
S s2{a, 2, 3}; // error: narrowing
S s3{}; // OK, invoke #2
// — end example]
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 о неопределённом поведении | |
Внешние ссылки
© cppreference.com
Licensed under the Creative Commons Attribution-ShareAlike Unported License v3.0.
https://en.cppreference.com/cpp/language/ub