Spec-Zone.ru › C++

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

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

Объяснение

Стандарт 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API