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


201

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

Скажи, как:

a[i] = i++;


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выражении

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

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

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


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

5

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

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

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

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


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 ++ разделяет эти основные поддержки.


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. Размер всех других типов зависит от платформы.


Разве это не то, для чего <cstdint>? Он определяет такие типы, как uint16_6 и так далее.
— Джаспер Беккерс

Да, но размер большинства типов, скажем, длинных, четко не определен.
— JaredPar

также cstdint еще не является частью текущего стандарта c ++. см. boost / stdint.hpp для текущего портативного решения.
— Эван Теран

Это не неопределенное поведение. Стандарт гласит, что соответствующая платформа определяет размеры, а не стандарт, определяющий их.
— Даниэль Уорвикер

1
@JaredPar: Это сложный пост с большим количеством нитей разговора, так что я подвел все это здесь . Суть в следующем: «5. Для представления -2147483647 и +2147483647 в двоичном виде вам нужно 32 бита».
— Джон Диблинг

2

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.