Namespaces
Variants

Undefined behavior

From cppreference.net

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

  • undefined behavior - на поведение программы не накладывается никаких ограничений. Примерами неопределенного поведения являются: обращение к памяти за пределами границ массива, переполнение знакового целого числа, разыменование нулевого указателя, изменение одного и того же скаляра более одного раза в выражении без точек следования, доступ к объекту через указатель другого типа и т.д. От компиляторов не требуется диагностировать неопределенное поведение (хотя многие простые ситуации диагностируются), и скомпилированная программа не обязана делать что-либо осмысленное.
  • неуточнённое поведение - допускаются два или более вариантов поведения, и реализация не обязана документировать эффекты каждого варианта. Например, порядок вычислений , являются ли идентичные строковые литералы различными и т.д. Каждое неуточнённое поведение приводит к одному из множества допустимых результатов и может давать разные результаты при повторении в той же программе.
  • implementation-defined behavior - неопределённое поведение, при котором каждая реализация документирует, как делается выбор. Например, количество битов в байте или является ли сдвиг вправо знаковых целых чисел арифметическим или логическим.
  • поведение, зависящее от локали - определяемое реализацией поведение, которое зависит от текущей выбранной локали . Например, возвращает ли islower true для любых символов, кроме 26 строчных латинских букв.

(Примечание: Строго соответствующие программы не зависят от любого неуказанного, неопределённого или определяемого реализацией поведения)

Компиляторы обязаны выдавать диагностические сообщения (либо ошибки, либо предупреждения) для любых программ, нарушающих синтаксические правила или семантические ограничения C++, даже если их поведение определено как неопределенное или определяемое реализацией, или если компилятор предоставляет языковое расширение, позволяющее принять такую программу. Диагностика для неопределенного поведения в остальных случаях не требуется.

Содержание

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

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

Например,

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

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

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

foo:
        mov     eax, 1
        ret

Выход за границы доступа

int table[4] = {0};
int exists_in_table(int v)
{
    // возвращает 1 в одной из первых 4 итераций или неопределённое поведение из-за выхода за границы массива
    for (int i = 0; i <= 4; i++)
        if (table[i] == v)
            return 1;
    return 0;
}

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

exists_in_table:
        mov     eax, 1
        ret

Неинициализированная скалярная переменная

_Bool p; // неинициализированная локальная переменная
if (p) // UB доступ к неинициализированной скалярной переменной
    puts("p is true");
if (!p) // UB доступ к неинициализированной скалярной переменной
    puts("p is false");

Может выдать следующий вывод (наблюдалось в старой версии gcc):

p — истина
p — ложь
size_t f(int x)
{
    size_t a;
    if (x) // либо x ненулевое, либо неопределённое поведение
        a = 42;
    return a;
}

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

f:
        mov     eax, 42
        ret

Недопустимый скаляр

int f(void)
{
    _Bool b = 0;
    unsigned char* p = (unsigned char*)&b;
    *p = 10;
    // чтение из b теперь является неопределённым поведением
    return b == 0;
}

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

f:
        mov     eax, 11
        ret

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

int foo(int* p)
{
    int x = *p;
    if (!p)
        return x; // Либо неопределенное поведение выше, либо эта ветвь никогда не выполняется
    else
        return 0;
}
int bar()
{
    int* p = NULL;
    return *p;    // Безусловное неопределенное поведение
}

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

foo:
        xor     eax, eax
        ret
bar:
        ret

Доступ к указателю, переданному в realloc

Выберите clang для просмотра показанного вывода

#include <stdio.h>
#include <stdlib.h>
int main(void)
{
    int *p = (int*)malloc(sizeof(int));
    int *q = (int*)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
        printf("%d%d\n", *p, *q);
}

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

12

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

Выберите clang для просмотра показанного вывода

#include <stdio.h>
int fermat()
{
    const int MAX = 1000;
    // Endless loop with no side effects is UB
    for (int a = 1, b = 1, c = 1; 1;)
    {
        if (((a * a * a) == ((b * b * b) + (c * c * c))))
            return 1;
        ++a;
        if (a > MAX)
        {
            a = 1;
            ++b;
        }
        if (b > MAX)
        {
            b = 1;
            ++c;
        }
        if (c > MAX)
            c = 1;
    }
    return 0;
}
int main(void)
{
    if (fermat())
        puts("Fermat's Last Theorem has been disproved.");
    else
        puts("Fermat's Last Theorem has not been disproved.");
}

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

Fermat's Last Theorem has been disproved.

Ссылки

  • Стандарт C23 (ISO/IEC 9899:2024):
  • 3.4 Поведение (стр.: TBD)
  • 4 Соответствие стандарту (стр.: TBD)
  • Стандарт C17 (ISO/IEC 9899:2018):
  • 3.4 Поведение (стр: 3-4)
  • 4 Соответствие стандарту (стр: 8)
  • Стандарт C11 (ISO/IEC 9899:2011):
  • 3.4 Поведение (стр: 3-4)
  • 4/2 Неопределенное поведение (стр: 8)
  • Стандарт C99 (ISO/IEC 9899:1999):
  • 3.4 Поведение (стр. 3-4)
  • 4/2 Неопределенное поведение (стр. 7)
  • Стандарт C89/C90 (ISO/IEC 9899:1990):
  • 1.6 ОПРЕДЕЛЕНИЯ ТЕРМИНОВ

Смотрите также

C++ documentation for Undefined behavior
Перевод:
C++ documentation для Undefined behavior

Внешние ссылки

1. Что каждый программист на C должен знать о неопределенном поведении #1/3
2. Что каждый программист на C должен знать о неопределенном поведении #2/3
3. Что каждый программист на C должен знать о неопределенном поведении #3/3
4. Неопределенное поведение может привести к путешествиям во времени (и другим вещам, но путешествия во времени самые интересные)
5. Понимание целочисленного переполнения в C/C++
6. Неопределенное поведение и Великая теорема Ферма
7. Забавы с NULL указателями, часть 1 (локальная уязвимость в Linux 2.6.30, вызванная UB из-за разыменования нулевого указателя)