Неопределённое поведение — cppreference.com
Материал из cppreference.com
Делает всю программу бессмысленной, если нарушаются определённые правила языка.
Объяснение
Стандарт 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, либо неопределённое поведение из-за знакового // переполнения }
может быть скомпилировано как (демонстрация)
Доступ вне границ
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; }
Может быть скомпилировано как (демонстрация)
Показанный результат наблюдался в одной из старых версий 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; }
Может быть скомпилировано как (демонстрация)
Разыменование нулевого указателя
Спорным остается вопрос о том, является ли простое разыменование нулевого указателя неопределённым поведением, смотрите 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'; }
Возможный вывод:
Бесконечный цикл без побочных эффектов
Выберите 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"; }
Возможный вывод:
Последняя Теорема Ферма опровергнута.
Смотрите также
Внешние ссылки
| 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. | Неопределённое поведение и Великая Теорема Ферма |