Неопределённое поведение
Делает всю программу бессмысленной, если нарушаются определённые правила языка.
Объяснение
Стандарт C++ точно определяет наблюдаемое поведение каждой программы на C++, которая не попадает ни в один из следующих классов:
- некорректна - программа имеет синтаксические ошибки или диагностируемые семантические ошибки. Для выдачи диагностики необходим соответствующий компилятор C++, даже если он определяет расширение языка, которое придаёт значение такому коду (например, с массивами переменной длины). В тексте стандарта используются слова должен, не должен и некорректный для обозначения этих требований.
- некорректна, диагностика не требуется - программа имеет семантические ошибки, которые в общем случае невозможно диагностировать (например, нарушения ODR или другие ошибки, которые можно обнаружить только во время компоновки). Поведение не определено, если такая программа выполняется.
- поведение, определяемое реализацией - поведение программы варьируется в зависимости от реализации, и соответствующая реализация должна документировать эффекты каждого поведения. Например, тип std::size_t или количество бит в байте, или текст std::bad_alloc::what. Подмножеством поведения, определяемого реализацией, является поведение, зависящее от локали, которое зависит от предоставленной реализацией локали.
- неуказанное поведение - поведение программы варьируется в зависимости от реализации, и от соответствующей реализации не требуется документирование эффектов каждого поведения. Например, порядок оценки, различны ли идентичные строковые литералы, объём накладных расходов на распределение массива и т.д. Каждое неуказанное поведение приводит к одному результату из набора допустимых.
- неопределённое поведение - нет ограничений на поведение программы. Примерами неопределённого поведения являются гонка данных, доступ к памяти за пределами массива, целочисленное переполнение со знаком, разыменование нулевого указателя, более одного изменения одного и того же скаляра в выражении без какой-либо промежуточной точки последовательности (до C++11), которые не имеют последовательности (начиная с C++11), доступ к объекту через указатель другого типа и т.д. Компиляторы не обязаны диагностировать неопределённое поведение (хотя диагностируются многие простые ситуации), а от скомпилированной программы не требуется делать что-либо значимое.
Неопределённое поведение и оптимизация
Поскольку корректные программы на C++ не имеют неопределённого поведения, компиляторы могут давать неожиданные результаты, когда программа, которая на самом деле имеет неопределённое поведение, скомпилирована с включенной оптимизацией:
Например,
Знаковое переполнение
int foo(int x) {
return x+1 > x; // либо true, либо неопределённое поведение из-за знакового
// переполнения
}
может быть скомпилировано как (демонстрация)
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;
}
Может быть скомпилировано как (демонстрация)
f(int):
mov eax, 11
ret
Показанный результат наблюдался в одной из старых версий gcc
#include <cstdio>
int main()
{
bool p; // неинициализированная локальная переменная
if(p) // неопределённое поведение при доступе к неинициализированному скаляру
std::puts("p равно true");
if(!p) // неопределённое поведение при доступе к неинициализированному скаляру
std::puts("p равно false");
}
Возможный вывод:
p равно true
p равно false
Неверный скаляр
int f() {
bool b = true;
unsigned char* p = reinterpret_cast<unsigned char*>(&b);
*p = 10;
// чтение из b теперь неопределённое поведение
return b == 0;
}
Может быть скомпилировано как (демонстрация)
f():
movl $11, %eax
ret
Разыменование нулевого указателя
Спорным остается вопрос о том, является ли простое разыменование нулевого указателя неопределённым поведением, смотрите CWG проблема 232. Примеры демонстрируют чтение по результату такого разыменования.
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 <iostream>
#include <cstdlib>
int main() {
int *p = (int*)std::malloc(sizeof(int));
int *q = (int*)std::realloc(p, sizeof(int));
*p = 1; // неопределённое поведение при доступе к указателю, который был передан
// в realloc
*q = 2;
if (p == q) // неопределённое поведение при доступе к указателю, который был передан
// в realloc
std::cout << *p << *q << '\n';
}
Возможный вывод:
12
Бесконечный цикл без побочных эффектов
Выберите clang или последнюю версию gcc, чтобы увидеть показанный вывод.
#include <cstdlib>
#include <iostream>
bool fermat() {
const int max_value = 1000;
int a=1,b=1,c=1;
// Бесконечный цикл без побочных эффектов это неопределённое поведение
for (int a = 1, b = 1, c = 1; true; )
{
if (((a*a*a) == ((b*b*b)+(c*c*c))))
return true; // опровергнуто :)
a++;
if (a>max_value) { a=1; b++; }
if (b>max_value) { b=1; c++; }
if (c>max_value) { c=1;}
}
return false; // не опровергнуто
}
int main() {
std::cout << "Последняя теорема Ферма ";
fermat()
? std::cout << "опровергнута.\n";
: std::cout << "не опровергнута.\n";
}
Возможный вывод:
Последняя Теорема Ферма опровергнута.
Смотрите также
Документация C по Неопределённое поведение
|
Внешние ссылки
| 1. | Блог Проекта LLVM: Что Каждый Программист на C Должен Знать о Неопределённом Поведении #1/3 |
| 2. | Блог Проекта LLVM: Что Каждый Программист на C Должен Знать о Неопределённом Поведении #2/3 |
| 3. | Блог Проекта LLVM: Что Каждый Программист на C Должен Знать о Неопределённом Поведении #3/3 |
| 4. | Неопределённое поведение может привести к путешествию во времени (среди прочего, путешествие во времени – самое забавное) |
| 5. | Понимание Целочисленного Переполнения в C/C++ |
| 6. | Веселье с NULL указателями, часть 1 (локальный эксплойт в Linux 2.6.30, вызванный неопределённым поведением из-за разыменования нулевого указателя) |
| 7. | Неопределённое поведение и Великая Теорема Ферма |