Неопределённое поведение
Приводит к бессмысленности всей программы, если нарушаются определённые правила языка.
Объяснение
Стандарт C++ точно определяет наблюдаемое поведение каждой программы C++, которая не попадает в один из следующих классов:
- Некорректная — программа содержит синтаксические ошибки или диагностируемые семантические ошибки. Соответствующий компилятор C++ должен выдать диагностическое сообщение, даже если он определяет расширение языка, которое присваивает смысл такому коду (например, с массивами переменной длины). В тексте стандарта используются термины должен, не должен и некорректная для обозначения этих требований.
- Некорректная, без требования диагностики — программа содержит семантические ошибки, которые в общем случае могут не быть диагностируемыми (например, нарушения ODR или другие ошибки, которые обнаруживаются только на этапе линковки). Поведение является неопределённым, если такая программа выполняется.
- Поведение, определяемое реализацией — поведение программы варьируется между реализациями, и соответствующая реализация должна документировать последствия каждого поведения. Например, тип
std::size_tили количество битов в байте, или текстstd::bad_alloc::what. Подмножество поведения, определяемого реализацией, — это поведение, зависящее от локали, которое зависит от предоставляемой реализацией локали. - Неопределённое поведение — поведение программы не ограничено. Примеры неопределённого поведения: гонки данных, доступ к памяти за пределами границ массива, переполнение знакового целого числа, обращение к нулевому указателю, несколько модификаций одного скаляра в выражении без промежуточной точки последовательности(до 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Некорректный скаляр
Обращение к нулевому указателю
Можно ли считать, что простое разыменование нулевого указателя — это неопределённое поведение, см. CWG issue 2823. Примеры демонстрируют чтение из результата такого разыменования.
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
Выберите 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;
// Endless 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 документация для Неопределённого поведения |
Внешние ссылки
| 1. | Блог проекта LLVM: Что каждый программист на C должен знать об неопределённом поведении #1/3 |
| 2. | Блог проекта LLVM: Что каждый программист на C должен знать об неопределённом поведении #2/3 |
| 3. | Блог проекта LLVM: Что каждый программист на C должен знать об неопределённом поведении #3/3 |
| 4. | Неопределённое поведение может привести к путешествиям во времени (наряду с другими вещами, но путешествия во времени самые интересные) |
| 5. | Понимание переполнения целых чисел в C/C++ |
| 6. | Развлечения с нулевыми указателями, часть 1 (локальный эксплойт в Linux 2.6.30, вызванный неопределённым поведением из-за разыменования нулевого указателя) |
| 7. | Неопределённое поведение и последняя теорема Ферма. |
© cppreference.com
Licensed under the Creative Commons Attribution-ShareAlike Unported License v3.0.
https://en.cppreference.com/w/cpp/language/ub