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

201

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

Скажи, как:

a[i] = i++;

yesraaj
источник
3
Ты уверен. Это выглядит четко определенным.
Мартин Йорк,
17
6.2.2 Порядок оценки [expr.evaluation] на языке программирования C ++ так сказать. У меня нет другой ссылки
yesraaj
4
Он прав ... только что посмотрел на 6.2.2 в языке программирования C ++ и там говорится, что v [i] = i ++ не определено
dancavallaro
4
Я хотел бы представить, потому что компилятор выполняет i ++ до или после вычисления места в памяти v [i]. конечно, меня всегда будут там назначать. но он может записывать либо в v [i], либо в v [i + 1] в зависимости от порядка операций ..
Эван Теран
2
Все, что говорит язык программирования C ++: «Порядок операций подвыражений в выражении не определен. В частности, нельзя предполагать, что выражение вычисляется слева направо».
dancavallaro

Ответы:

233

Указатель

  • Разыменование 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выражении

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

  • Вызов выхода при уничтожении программы со статической продолжительностью хранения
Diomidis Spinellis
источник
Hm ... NaN (x / 0) и Infinity (0/0) были охвачены IEE 754, если C ++ был разработан позже, почему он записывает x / 0 как неопределенное?
new123456
Re: «Обратная косая черта, за которой следует символ, который не является частью указанных escape-кодов в символе или строковой константе.» Это UB в C89 (§3.1.3.4) и C ++ 03 (который включает в себя C89), но не в C99. C99 говорит, что «результат не является токеном и требуется диагностика» (§6.4.4.4). Предположительно C ++ 0x (который включает в себя C89) будет таким же.
Адам Розенфилд
1
Стандарт C99 содержит список неопределенного поведения в приложении J.2. Потребовалось бы немного поработать, чтобы адаптировать этот список к C ++. Вам придется изменить ссылки на правильные предложения C ++, а не предложения C99, удалить что-либо не относящееся к делу, а также проверить, действительно ли все эти вещи не определены в C ++ и C. Но это обеспечивает начало.
Стив Джессоп
1
@ new123456 - не все модули с плавающей запятой совместимы с IEE754. Если C ++ требует соответствия IEE754, компиляторы должны будут проверить и обработать случай, когда RHS равен нулю, посредством явной проверки. Сделав поведение неопределенным, компилятор может избежать этих издержек, сказав, что «если вы используете не IEE754 FPU, вы не получите IEEE754 FPU».
SecurityMatt
1
«Оценка выражения, результат которого находится вне диапазона соответствующих типов» .... переполнение целых чисел четко определено для целочисленных типов UNSIGNED, но не для знаковых.
nacitar sevaht
31

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

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


Это:

// 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);

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

Мартин Йорк
источник
23
Порядок не определен, а не неопределен.
Роб Кеннеди
1
Я ненавижу это :) Я потерял рабочий день, когда выследил один из этих случаев ... в любом случае, усвоил урок и к счастью больше не упал
Роберт Гулд
2
@Rob: Я бы поспорил с вами об изменении значения здесь, но я знаю, что комитет по стандартам очень требователен к точному определению этих двух слов. Так что я просто изменю это :-)
Мартин Йорк
2
Мне повезло в этом. Я был укушен этим, когда учился в колледже, и у меня был профессор, который взглянул на него и рассказал мне о моей проблеме примерно за 5 секунд. Невозможно сказать, сколько времени я бы потратил впустую на отладку.
Билл Ящерица
27

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

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

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.
Мартин Йорк
источник
что означает точка последовательности?
Yesraaj
en.wikipedia.org/wiki/Sequence_point
Мартин Йорк,
1
Ох ... это противно, тем более что я видел эту точную структуру, рекомендованную в Java
Том
Обратите внимание, что некоторые компиляторы определяют поведение в этой ситуации. Например, в VC ++ 2005+, если a является энергозависимым, необходимые барьеры памяти устанавливаются для предотвращения переупорядочения инструкций, чтобы работала двойная проверка блокировки.
Затмение
Мартин Йорк: <i> // (c) гарантированно произойдет после (a) и (b) </ i> Это так? По общему признанию в этом конкретном примере единственный сценарий, где это могло бы иметь значение, был бы, если бы 'i' была изменчивой переменной, сопоставленной с аппаратным регистром, и [i] (старое значение 'i') было псевдонимом к нему, но есть ли гарантировать, что приращение произойдет до точки последовательности?
суперкат
5

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

Дэниел Уорвикер
источник
Сделано это раньше, но я не вижу, как это неопределено. Совершенно очевидно, что вы делаете бесконечную рекурсию в запоздалой мысли.
Роберт Гулд
Проблема в том, что компилятор не может проверить ваш код и точно решить, будет ли он страдать от бесконечной рекурсии или нет. Это пример проблемы остановки. См .: stackoverflow.com/questions/235984/…
Даниэль Эрвикер
Да, это определенно проблема остановки
Роберт Гулд
это вызвало сбой моей системы из-за перестановки, вызванной слишком малым объемом памяти.
Йоханнес Шауб -
2
Константы препроцессора, которые не вписываются в int, также являются временем компиляции.
Джошуа
5

Присваивание константе после удаления constс помощью const_cast<>:

const int i = 10; 
int *p =  const_cast<int*>( &i );
*p = 1234; //Undefined
yesraaj
источник
5

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

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

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

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

Constantin
источник
4

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

int i =1;
i = ++i;

// Undefined. Assignment to 'i' twice in the same expression.
Мартин Йорк
источник
По крайней мере, один раз между двумя точками последовательности.
Прасун Саурав
2
@Prasoon: я думаю, что вы имели в виду: не более одного раза между двумя точками последовательности. :-)
Наваз
3

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

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

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

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

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

RandomNickName42
источник
9
Если вы достигнете этих пределов, у вас будет больше проблем, чем неопределенного поведения.
new123456
Вы можете легко превысить 65535 байт в объекте, например, STD :: vector
Demi
2

Используется 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.

Джон Диблинг
источник
2

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

JaredPar
источник
Разве это не то, для чего <cstdint>? Он определяет такие типы, как uint16_6 и так далее.
Джаспер Беккерс
Да, но размер большинства типов, скажем, длинных, четко не определен.
JaredPar
также cstdint еще не является частью текущего стандарта c ++. см. boost / stdint.hpp для текущего портативного решения.
Эван Теран
Это не неопределенное поведение. Стандарт гласит, что соответствующая платформа определяет размеры, а не стандарт, определяющий их.
Даниэль Уорвикер
1
@JaredPar: Это сложный пост с большим количеством нитей разговора, так что я подвел все это здесь . Суть в следующем: «5. Для представления -2147483647 и +2147483647 в двоичном виде вам нужно 32 бита».
Джон Диблинг
2

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

yesraaj
источник