close
Пространства имён
Варианты
Действия

Неопределённое поведение

Материал из cppreference.com
 
 
Язык С++
Общие темы
Управление потоком
Операторы условного выполнения
if
Операторы итерации (циклы)
Операторы перехода
Функции
Объявление функции
Выражение лямбда-функции
Спецификатор inline
Спецификации динамических исключений (до C++17*)
Спецификатор noexcept (C++11)
Исключения
Пространства имён
Типы
Спецификаторы
decltype (C++11)
auto (C++11)
alignas (C++11)
Спецификаторы длительности хранения
Инициализация
Выражения
Альтернативные представления
Литералы
Логические - Целочисленные - С плавающей запятой
Символьные - Строковые - nullptr (C++11)
Определяемые пользователем (C++11)
Утилиты
Атрибуты (C++11)
Types
Объявление typedef
Объявление псевдонима типа (C++11)
Casts
Неявные преобразования - Явные преобразования
static_cast - dynamic_cast
const_cast - reinterpret_cast
Выделение памяти
Классы
Свойства функции класса
explicit (C++11)
static
Специальные функции-элементы
Шаблоны
Разное
 
 

Делает всю программу бессмысленной, если нарушаются определённые правила языка.

Объяснение

Стандарт 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.  Неопределённое поведение и Великая Теорема Ферма