Spec-Zone.ru › C++

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

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

Пояснение

Стандарт C++ точно определяет наблюдаемое поведение каждой программы на C++, которая не относится к одному из следующих классов:

  • некорректная программа - В программе есть синтаксические ошибки или диагностируемые семантические ошибки.
    • Соответствующий стандарту компилятор C++ обязан выдать диагностическое сообщение, даже если он определяет расширение языка, придающее смысл такому коду (например, массивам переменной длины).
    • В тексте стандарта для обозначения этих требований используются слова shall, shall not и ill-formed.
  • некорректная программа, диагностическое сообщение не требуется - В программе есть семантические ошибки, которые в общем случае могут быть недиагностируемыми (например, нарушения ODR или другие ошибки, обнаруживаемые только во время компоновки).
    • Если такая программа выполняется, поведение является неопределённым.
  • поведение, определяемое реализацией - Поведение программы различается в разных реализациях, и соответствующая стандарту реализация должна документировать последствия каждого варианта поведения.
    • Например, тип std::size_t, количество битов в байте или текст std::bad_alloc::what.
    • Подмножество поведения, определяемого реализацией, составляет поведение, зависящее от локали, которое зависит от предоставленной реализацией локали.
  • неуточнённое поведение - Поведение программы различается в разных реализациях, и соответствующая стандарту реализация не обязана документировать последствия каждого варианта поведения.
    • Например, порядок вычисления, являются ли идентичные строковые литералы различными, объём накладных расходов на выделение памяти для массивов и т. д.
    • Каждый вариант неуточнённого поведения приводит к одному из набора допустимых результатов.
  • ошибочное поведение - (Некорректное) поведение, которое реализации рекомендуется диагностировать.
    • Ошибочное поведение всегда является следствием некорректного кода программы.
    • Вычисление константного выражения никогда не приводит к ошибочному поведению.
    • Если выполнение содержит операцию, для которой указано ошибочное поведение, реализации разрешено и рекомендуется выдать диагностическое сообщение, а также разрешено завершить выполнение в неопределённый момент после этой операции.
    • Реализация может выдать диагностическое сообщение, если она может определить, что ошибочное поведение достижимо при некотором зависящем от реализации наборе предположений о поведении программы; при этом возможны ложные срабатывания.
Примеры ошибочного поведения
#include <cassert>
#include <cstring>

void f()
{   
    int d1, d2;       // d1, d2 have erroneous values
    int e1 = d1;      // erroneous behavior
    int e2 = d1;      // erroneous behavior
    assert(e1 == e2); // holds
    assert(e1 == d1); // holds, erroneous behavior
    assert(e2 == d1); // holds, erroneous behavior

    std::memcpy(&d2, &d1, sizeof(int)); // no erroneous behavior, but
                                        // d2 has an erroneous value

    assert(e1 == d2); // holds, erroneous behavior
    assert(e2 == d2); // holds, erroneous behavior
}

unsigned char g(bool b)
{
    unsigned char c;     // c has erroneous value
    unsigned char d = c; // no erroneous behavior, but d has an erroneous value
    assert(c == d);      // holds, both integral promotions have erroneous behavior
    int e = d;           // erroneous behavior
    return b ? d : 0;    // erroneous behavior if b is true
}
(начиная с C++26)
  • неопределённое поведение - Поведение программы ничем не ограничено.
    • Некоторые примеры неопределённого поведения: гонки данных, доступ к памяти за пределами массива, переполнение знакового целого числа, разыменование нулевого указателя, более одной модификации одного и того же скалярного объекта в выражении без промежуточной точки следования(до C++11)без упорядочения(начиная с 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

Разыменование нулевого указателя

В примерах показано чтение результата разыменования нулевого указателя.

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

#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

Бесконечный цикл без побочных эффектов

#include <iostream>

bool fermat()
{
    const int max_value = 1000;

    // Non-trivial infinite 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++ требует только диагностического сообщения (предупреждения компилятора), если только программа не является «некорректной, но диагностическое сообщение не требуется».

Например, если расширения языка не отключены с помощью --pedantic-errors, GCC скомпилирует следующий пример только с предупреждением, хотя он приведён в стандарте C++ как пример «ошибки» (см. также GCC Bugzilla #55783)

#include <iostream>

// Example tweak, do not use constant
double a{1.0};

// C++23 standard, §9.4.5 List-initialization [dcl.init.list], Example #6:
struct S
{
    // no initializer-list constructors
    S(int, double, double); // #1
    S();                    // #2
    // ...
};

S s1 = {1, 2, 3.0}; // OK, invoke #1
S s2{a, 2, 3}; // error: narrowing
S s3{}; // OK, invoke #2
// — end example]

S::S(int, double, double) {}
S::S() {}

int main()
{
    std::cout << "All checks have passed.\n";
}

Возможный вывод:

main.cpp:17:6: error: type 'double' cannot be narrowed to 'int' in initializer ⮠
list [-Wc++11-narrowing]
S s2{a, 2, 3}; // error: narrowing
     ^
main.cpp:17:6: note: insert an explicit cast to silence this issue
S s2{a, 2, 3}; // error: narrowing
     ^
     static_cast<int>( )
1 error generated.

Ссылки

Дополнительное содержимое
  • Стандарт C++23 (ISO/IEC 14882:2024):
    • 3.25 некорректная программа [defns.ill.formed]
    • 3.26 поведение, определяемое реализацией [defns.impl.defined]
    • 3.66 неуточнённое поведение [defns.unspecified]
    • 3.68 корректная программа [defns.well.formed]
  • Стандарт C++20 (ISO/IEC 14882:2020):
    • TBD некорректная программа [defns.ill.formed]
    • TBD поведение, определяемое реализацией [defns.impl.defined]
    • TBD неуточнённое поведение [defns.unspecified]
    • TBD корректная программа [defns.well.formed]
  • Стандарт C++17 (ISO/IEC 14882:2017):
    • TBD некорректная программа [defns.ill.formed]
    • TBD поведение, определяемое реализацией [defns.impl.defined]
    • TBD неуточнённое поведение [defns.unspecified]
    • TBD корректная программа [defns.well.formed]
  • Стандарт C++14 (ISO/IEC 14882:2014):
    • TBD некорректная программа [defns.ill.formed]
    • TBD поведение, определяемое реализацией [defns.impl.defined]
    • TBD неуточнённое поведение [defns.unspecified]
    • TBD корректная программа [defns.well.formed]
  • Стандарт C++11 (ISO/IEC 14882:2011):
    • TBD некорректная программа [defns.ill.formed]
    • TBD поведение, определяемое реализацией [defns.impl.defined]
    • TBD неуточнённое поведение [defns.unspecified]
    • TBD корректная программа [defns.well.formed]
  • Стандарт C++98 (ISO/IEC 14882:1998):
    • TBD некорректная программа [defns.ill.formed]
    • TBD поведение, определяемое реализацией [defns.impl.defined]
    • TBD неуточнённое поведение [defns.unspecified]
    • TBD корректная программа [defns.well.formed]

См. также

[[assume(expression)]]
(C++23)
указывает, что выражение всегда будет вычисляться в true в заданной точке
(спецификатор атрибута)
[[indeterminate]]
(C++26)
указывает, что объект имеет неопределённое значение, если он не инициализирован
(спецификатор атрибута)
unreachable
(C++23)
обозначает недостижимую точку выполнения
(функция)
Документация по 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. Неопределённое поведение и Великая теорема Ферма
8. Руководство программиста на C++ по неопределённому поведению

© cppreference.com
Licensed under the Creative Commons Attribution-ShareAlike Unported License v3.0.
https://en.cppreference.com/cpp/language/ub

Spec-Zone.ru

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