Каковы все распространенные неопределенные поведения, о которых должен знать программист C++?

Каковы все распространенные неопределенные поведения, о которых должен знать программист C++?

Скажи, как:

a[i] = i++;

11 ответов

Решение

Указатель

  • Разыменование NULL указатель
  • Разыменование указателя, возвращаемого "новым" распределением нулевого размера
  • Использование указателей на объекты, срок жизни которых истек (например, стек выделенных объектов или удаленных объектов)
  • Разыменование указателя, который еще не был определенно инициализирован
  • Выполнение арифметики с указателями, которая дает результат за пределами (выше или ниже) массива.
  • Разыменование указателя в месте за концом массива.
  • Преобразование указателей в объекты несовместимых типов
  • С помощью memcpy копировать перекрывающиеся буферы.

Переполнение буфера

  • Чтение или запись в объект или массив с отрицательным смещением или превышающим размер этого объекта (переполнение стека / кучи)

Целочисленные переполнения

  • Целочисленное переполнение со знаком
  • Оценка выражения, которое не определено математически
  • Смещение влево на отрицательную величину (смещение вправо на отрицательную величину определяется реализацией)
  • Сдвиг значений на величину, большую или равную количеству битов в числе (например, int64_t i = 1; i <<= 72 не определено)

Типы, Cast и Const

  • Преобразование числового значения в значение, которое не может быть представлено целевым типом (напрямую или через static_cast)
  • Использование автоматической переменной до того, как она была определенно назначена (например, int i; i++; cout << i;)
  • Используя значение любого объекта типа кроме volatile или же sig_atomic_t при получении сигнала
  • Попытка изменить строковый литерал или любой другой объект const во время его жизни
  • Конкатенация узкого с широким строковым литералом во время предварительной обработки

Функция и шаблон

  • Не возвращать значение из функции, возвращающей значение (напрямую или путем вытекания из блока try)
  • Несколько разных определений для одной и той же сущности (класс, шаблон, перечисление, встроенная функция, статическая функция-член и т. Д.)
  • Бесконечная рекурсия в создании шаблонов
  • Вызов функции с использованием различных параметров или связей с параметрами и связями, которые определены как функции.

OOP

  • Каскадные разрушения объектов со статической длительностью хранения
  • Результат присвоения частично перекрывающимся объектам
  • Рекурсивный повторный вход в функцию во время инициализации ее статических объектов
  • Выполнение виртуальных функций вызывает чисто виртуальные функции объекта из его конструктора или деструктора
  • Обращаясь к нестатическим членам объектов, которые не были построены или уже разрушены

Исходный файл и предварительная обработка

  • Непустой исходный файл, который не заканчивается новой строкой или заканчивается обратной косой чертой (до C++11)
  • Обратная косая черта, за которой следует символ, который не является частью указанных escape-кодов в символе или строковой константе (это определяется реализацией в C++11).
  • Превышение пределов реализации (количество вложенных блоков, количество функций в программе, доступное пространство стека...)
  • Числовые значения препроцессора, которые не могут быть представлены long int
  • Директива предварительной обработки в левой части функционально-подобного определения макроса
  • Динамическая генерация определенного токена в #if выражение

Быть классифицированным

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

Порядок оценки параметров функции - неопределенное поведение. (Это не приведет к сбою, взрыву или заказу пиццы в вашей программе... в отличие от неопределенного поведения.)

Единственное требование - все параметры должны быть полностью оценены перед вызовом функции.


Это:

// The simple obvious one.
callFunc(getA(),getB());

Может быть эквивалентно этому:

int a = getA();
int b = getB();
callFunc(a,b);

Или это:

int b = getB();
int a = getA();
callFunc(a,b);

Это может быть либо; дело за компилятором. Результат может иметь значение, в зависимости от побочных эффектов.

Компилятор может переупорядочить части выражения выражения (при условии, что значение не изменилось).

Из оригинального вопроса:

a[i] = i++;

// This expression has three parts:
(a) a[i]
(b) i++
(c) Assign (b) to (a)

// (c) is guaranteed to happen after (a) and (b)
// But (a) and (b) can be done in either order.
// See n2521 Section 5.17
// (b) increments i but returns the original value.
// See n2521 Section 5.2.6
// Thus this expression can be written as:

int rhs  = i++;
int lhs& = a[i];
lhs = rhs;

// or
int lhs& = a[i];
int rhs  = i++;
lhs = rhs;

Двойная проверка блокировки. И одна легкая ошибка, чтобы сделать.

A* a = new A("plop");

// Looks simple enough.
// But this can be split into three parts.
(a) allocate Memory
(b) Call constructor
(c) Assign value to 'a'

// No problem here:
// The compiler is allowed to do this:
(a) allocate Memory
(c) Assign value to 'a'
(b) Call constructor.
// This is because the whole thing is between two sequence points.

// So what is the big deal.
// Simple Double checked lock. (I know there are many other problems with this).
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        a = new A("Plop");  // (Point A).
    }
}
a->doStuff();

// Think of this situation.
// Thread 1: Reaches point A. Executes (a)(c)
// Thread 1: Is about to do (b) and gets unscheduled.
// Thread 2: Reaches point B. It can now skip the if block
//           Remember (c) has been done thus 'a' is not NULL.
//           But the memory has not been initialized.
//           Thread 2 now executes doStuff() on an uninitialized variable.

// The solution to this problem is to move the assignment of 'a'
// To the other side of the sequence point.
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        A* tmp = new A("Plop");  // (Point A).
        a = tmp;
    }
}
a->doStuff();

// Of course there are still other problems because of C++ support for
// threads. But hopefully these are addresses in the next standard.

Мой фаворит - "Бесконечная рекурсия в создании шаблонов", потому что я считаю, что это единственный случай, когда неопределенное поведение происходит во время компиляции.

Присвоение константе после зачистки constНесс с использованием const_cast<>:

const int i = 10; 
int *p =  const_cast<int*>( &i );
*p = 1234; //Undefined

Помимо неопределенного поведения, существует также такое же неприятное поведение, определяемое реализацией.

Неопределенное поведение происходит, когда программа делает что-то, результат которого не указан стандартом.

Поведение, определяемое реализацией, - это действие программы, результат которого не определен стандартом, но реализация должна быть задокументирована. Пример "Многобайтовые символьные литералы" из вопроса переполнения стека. Есть ли компилятор C, который не может это скомпилировать?,

Поведение, определяемое реализацией, кусает вас только тогда, когда вы начинаете портировать (но обновление до новой версии компилятора также портирует!)

Переменные могут быть обновлены только один раз в выражении (технически один раз между точками последовательности).

int i =1;
i = ++i;

// Undefined. Assignment to 'i' twice in the same expression.

Основное понимание различных экологических ограничений. Полный список приведен в разделе 5.2.4.1 спецификации C. Вот несколько из них;

  • 127 параметров в одном определении функции
  • 127 аргументов в одном вызове функции
  • 127 параметров в одном макроопределении
  • 127 аргументов в одном вызове макроса
  • 4095 символов в строке логического источника
  • 4095 символов в строковом литерале или широком строковом литерале (после объединения)
  • 65535 байт в объекте (только в размещенной среде)
  • 15 уровней для # включенных файлов
  • 1023 метки регистра для оператора switch (исключая метки для anynested операторов switch)

На самом деле я был немного удивлен ограничением в 1023 метки падежа для оператора switch, я могу предвидеть, что его превышение для сгенерированного кода /lex/parsers довольно легко.

Если эти пределы превышены, у вас есть неопределенное поведение (сбои, недостатки безопасности и т. Д.).

Да, я знаю, что это из спецификации C, но C++ разделяет эти основные поддержки.

С помощью memcpy копировать между перекрывающимися областями памяти. Например:

char a[256] = {};
memcpy(a, a, sizeof(a));

Поведение не определено в соответствии со стандартом C, который включен в стандарт C++03.

7.21.2.1 Функция memcpy

конспект

1/ #include void *memcpy(void * ограничение s1, const void * ограничение s2, size_t n);

Описание

2/ Функция memcpy копирует n символов из объекта, на который указывает s2, в объект, на который указывает s1. Если копирование происходит между объектами, которые перекрываются, поведение не определено. Возвращает 3 Функция memcpy возвращает значение s1.

7.21.2.2 Функция memmove

конспект

1 #include void *memmove(void *s1, const void *s2, size_t n);

Описание

2 Функция memmove копирует n символов из объекта, на который указывает s2, в объект, на который указывает s1. Копирование происходит так, как будто n символов из объекта, на который указывает s2, сначала копируются во временный массив из n символов, который не перекрывает объекты, на которые указывают s1 и s2, а затем n символов из временного массива копируются в объект, на который указывает s1. Возвращает

3 Функция memmove возвращает значение s1.

Единственный тип, для которого C++ гарантирует размер, это char, И размер равен 1. Размер всех других типов зависит от платформы.

Объекты уровня пространства имен в разных единицах компиляции никогда не должны зависеть друг от друга при инициализации, поскольку их порядок инициализации не определен.

Другие вопросы по тегам