Namespaces
Variants

Undefined behavior

From cppreference.net
C++ language
General topics
Flow control
Conditional execution statements
Iteration statements (loops)
Jump statements
Functions
Function declaration
Lambda function expression
inline specifier
Dynamic exception specifications ( until C++17* )
noexcept specifier (C++11)
Exceptions
Namespaces
Types
Specifiers
constexpr (C++11)
consteval (C++20)
constinit (C++20)
Storage duration specifiers
Initialization
Expressions
Alternative representations
Literals
Boolean - Integer - Floating-point
Character - String - nullptr (C++11)
User-defined (C++11)
Utilities
Attributes (C++11)
Types
typedef declaration
Type alias declaration (C++11)
Casts
Memory allocation
Classes
Class-specific function properties
Special member functions
Templates
Miscellaneous

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

Содержание

Объяснение

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

  • ill-formed - Программа содержит синтаксические ошибки или диагностируемые семантические ошибки.
  • Соответствующий компилятор C++ обязан выдать диагностическое сообщение, даже если он определяет языковое расширение, придающее значение такому коду (как в случае с массивами переменной длины).
  • Текст стандарта использует термины shall , shall not , и ill-formed для обозначения этих требований.
  • некорректная программа, диагностика не требуется - Программа содержит семантические ошибки, которые в общем случае могут быть не диагностируемыми (например, нарушения ODR или другие ошибки, обнаруживаемые только на этапе компоновки).
  • Поведение не определено, если такая программа выполняется.
  • implementation-defined behavior - Поведение программы варьируется между реализациями, и соответствующая реализация должна документировать эффекты каждого поведения.
  • Например, тип std::size_t или количество битов в байте, или текст std::bad_alloc::what .
  • Подмножеством определяемой реализацией поведения является локале-зависимое поведение , которое зависит от предоставленной реализацией локали .
  • неопределённое поведение - Поведение программы варьируется между реализациями, и соответствующая реализация не обязана документировать эффекты каждого поведения.
  • Например, order of evaluation , являются ли идентичные string literals различными, объем накладных расходов при выделении массива и т.д.
  • Каждое неопределенное поведение приводит к одному из множества допустимых результатов.
  • erroneous behavior - (Некорректное) поведение, которое рекомендуется диагностировать реализацией.
  • Ошибочное поведение всегда является следствием некорректного программного кода.
  • Вычисление константного выражения никогда не приводит к ошибочному поведению.
  • Если выполнение содержит операцию, определенную как имеющую ошибочное поведение, реализация имеет право и рекомендуется выдавать диагностическое сообщение, а также может завершить выполнение в неопределенный момент времени после этой операции.
  • Реализация может выдавать диагностическое сообщение, если она может определить, что ошибочное поведение достижимо при определенном наборе предположений о поведении программы, что может приводить к ложным срабатываниям.
Примеры ошибочного поведения
#include <cassert>
#include <cstring>
void f()
{   
    int d1, d2;       // d1, d2 имеют ошибочные значения
    int e1 = d1;      // ошибочное поведение
    int e2 = d1;      // ошибочное поведение
    assert(e1 == e2); // выполняется
    assert(e1 == d1); // выполняется, ошибочное поведение
    assert(e2 == d1); // выполняется, ошибочное поведение
    std::memcpy(&d2, &d1, sizeof(int)); // нет ошибочного поведения, но
                                        // d2 имеет ошибочное значение
    assert(e1 == d2); // выполняется, ошибочное поведение
    assert(e2 == d2); // выполняется, ошибочное поведение
}
unsigned char g(bool b)
{
    unsigned char c;     // c имеет ошибочное значение
    unsigned char d = c; // нет ошибочного поведения, но d имеет ошибочное значение
    assert(c == d);      // выполняется, оба целочисленных преобразования имеют ошибочное поведение
    int e = d;           // ошибочное поведение
    return b ? d : 0;    // ошибочное поведение, если b истинно
}
(начиная с C++26)
  • неопределённое поведение - Нет никаких ограничений на поведение программы.
  • Некоторые примеры неопределённого поведения: гонки данных, доступ к памяти за пределами границ массива, переполнение знакового целого числа, разыменование нулевого указателя, множественные модификации одного скаляра в выражении без промежуточных точек следования (до C++11) которые являются неупорядоченными (начиная с C++11) , доступ к объекту через указатель другого типа и т.д.
  • Реализации не обязаны диагностировать неопределённое поведение (хотя многие простые случаи диагностируются), и скомпилированная программа не обязана выполнять какие-либо осмысленные действия.
  • поведение, неопределённое в runtime - Поведение, которое является неопределённым, за исключением случаев, когда оно возникает во время вычисления выражения как core constant expression .
(since C++11)

НО и оптимизация

Поскольку корректные программы на C++ не содержат неопределённого поведения, компиляторы могут выдавать неожиданные результаты при компиляции программы с фактическим UB с включённой оптимизацией:

Например,

Переполнение знаковых чисел

int foo(int x)
{
    return x + 1 > x; // либо истина, либо неопределённое поведение из-за переполнения знакового числа
}

может быть скомпилирован как ( demo )

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;
}

Может быть скомпилировано как ( demo )

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;
    // чтение из b теперь является неопределенным поведением
    return b == 0;
}

Может быть скомпилировано как ( demo )

f():
        mov     eax, 11
        ret

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

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

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 <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;
    // 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>
// Пример изменения, не используйте константу
double a{1.0};
// Стандарт C++23, §9.4.5 List-initialization [dcl.init.list], Пример #6:
struct S
{
    // нет конструкторов с initializer-list
    S(int, double, double); // #1
    S();                    // #2
    // ...
};
S s1 = {1, 2, 3.0}; // OK, вызывает #1
S s2{a, 2, 3}; // ошибка: сужение типа
S s3{}; // OK, вызывает #2
// — конец примера]
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 в заданной точке
(спецификатор атрибута)
(C++26)
указывает, что объект имеет неопределённое значение, если он не инициализирован
(спецификатор атрибута)
отмечает недостижимую точку выполнения
(функция)
Документация 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. Неопределенное поведение и Великая теорема Ферма
8. Руководство программиста на C++ по неопределенному поведению