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

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

Скажите, например:

a[i] = i++;


person yesraaj    schedule 15.12.2008    source источник
comment
Уверены ли вы. Это выглядит хорошо определенным.   -  person Martin York    schedule 15.12.2008
comment
6.2.2 Порядок оценки [expr.evaluation] в языке программирования C ++ так сказано. У меня нет других ссылок   -  person yesraaj    schedule 15.12.2008
comment
Он прав .. только что посмотрел на 6.2.2 на языке программирования C ++, и там сказано, что v [i] = i ++ не определено.   -  person dancavallaro    schedule 15.12.2008
comment
Я могу представить, потому что компилятор заставляет выполнить i ++ до или после вычисления местоположения в памяти v [i]. конечно, я всегда буду там назначен. но он мог писать либо в v [i], либо в v [i + 1] в зависимости от порядка операций ..   -  person Evan Teran    schedule 15.12.2008
comment
Мартин, переменная обновится через некоторое время после чтения справа. Это может быть до или после чтения переменной для оценки левой стороны.   -  person Rob Kennedy    schedule 15.12.2008
comment
@ Роб: разве я не это только что сказал?   -  person Evan Teran    schedule 15.12.2008
comment
В качестве примера: он может выглядеть следующим образом: (psuedo asm): lea edi, [v + i]; mov [edi], я; inc i или mov eax, i; inc i; ле эди, [v + i]; mov [edi], eax - Эван Теран   -  person Evan Teran    schedule 15.12.2008
comment
Все, что говорит язык программирования C ++, - это то, что порядок операций над частями выражения внутри выражения не определен. В частности, вы не можете предполагать, что выражение вычисляется слева направо.   -  person dancavallaro    schedule 15.12.2008
comment
В ПОРЯДКЕ. После прочтения n2521 Раздел 5.2.6 думаю, что он у меня есть. Смотрите мой ответ ниже.   -  person Martin York    schedule 15.12.2008
comment
В наши дни для модератора кажется обрядом принудительного закрытия популярных вопросов, даже тех (таких как этот), которые относятся к серой зоне (и, следовательно, должны решаться сообществом в целом, ни одного заядлого модератора).   -  person BlueRaja - Danny Pflughoeft    schedule 27.07.2013
comment
Собственно, это кажется мне прекрасным примером хорошего вклада в SO.   -  person Kristian D'Amato    schedule 27.10.2013


Ответы (11)


Указатель

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

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

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

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

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

Типы, приведение и константа

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

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

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

ООП

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

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

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

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

  • Вызов exit при уничтожении программы со статической продолжительностью хранения
person Community    schedule 15.12.2008
comment
Хм ... NaN (x / 0) и Infinity (0/0) были охвачены IEE 754, если C ++ был разработан позже, почему он записывает x / 0 как undefined? - person new123456; 05.04.2011
comment
Re: обратная косая черта, за которой следует символ, который не является частью указанных escape-кодов в символьной или строковой константе. Это UB в C89 (§3.1.3.4) и C ++ 03 (который включает C89), но не в C99. C99 говорит, что результат не является токеном и требуется диагностика (§6.4.4.4). Предположительно C ++ 0x (который включает C89) будет таким же. - person Adam Rosenfield; 07.06.2011
comment
В стандарте C99 есть список неопределенного поведения в приложении J.2. Чтобы адаптировать этот список к C ++, потребуется некоторая работа. Вам нужно будет изменить ссылки на правильные предложения C ++, а не предложения C99, удалить все, что не имеет отношения к делу, а также проверить, действительно ли все эти вещи не определены в C ++, а также в C. Но это дает начало. - person Steve Jessop; 07.06.2011
comment
Довольно полезный и внушительный список. Мне нужно добавить это, так как он меня просто обжег: размер перечисления не определен; он должен быть достаточно большим, чтобы содержать int. Каждый перечислимый тип должен быть совместим с char, целочисленным типом со знаком или целочисленным типом без знака. Выбор типа определяется реализацией, но должен обеспечивать представление значений всех членов перечисления. - person Lolo; 09.08.2012
comment
@ new123456 - не все модули с плавающей запятой совместимы с IEE754. Если C ++ требует соответствия IEE754, компиляторам необходимо будет протестировать и обработать случай, когда RHS равен нулю, с помощью явной проверки. Сделав поведение неопределенным, компилятор может избежать этих накладных расходов, заявив, что если вы используете FPU, отличный от IEE754, вы не получите поведения IEEE754 FPU. - person SecurityMatt; 01.09.2012
comment
Сдвиг значений на величину, превышающую размер контейнера log2 (например, __int64 i = (37 ‹AC72) не определено). Это немного вводит в заблуждение и не всегда соответствует действительности. Это неопределенное поведение только в том случае, если sizeof(int) * CHAR_BIT <= 72 истинно. Объект, в который вы помещаете результат 37 << 72, не имеет значения, само выражение вызывает неопределенное поведение. Например, uint8_t n = (1 << 15); прекрасно. Сначала вы оцениваете 1 << 15, что составляет 2 ** 15. Результатом является затем неявное приведение от int к uint8_t, что означает, что n == 0 (из-за модульной арифметики) - person David Stone; 21.02.2013
comment
Вычисление выражения, результат которого не входит в диапазон соответствующих типов .... целочисленное переполнение четко определено для целочисленных типов UNSIGNED, но только не со знаком. - person nacitar sevaht; 10.08.2013
comment
Я как раз собирался сделать тот же комментарий, что и @nacitarsevaht. - person user541686; 20.08.2013

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

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


Этот:

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

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

person Martin York    schedule 15.12.2008
comment
Порядок не указан, не определен. - person Rob Kennedy; 15.12.2008
comment
Я ненавижу это :) Я потерял день работы, разыскивая один из этих случаев ... в любом случае выучил урок и, к счастью, больше не упал - person Robert Gould; 15.12.2008
comment
@Rob: Я бы поспорил с вами об изменении значения здесь, но я знаю, что комитет по стандартам очень придирчив к точному определению этих двух слов. Так что я просто изменю :-) - person Martin York; 15.12.2008
comment
Мне повезло с этим. Меня это укусило, когда я учился в колледже, и у меня был профессор, который взглянул на это и рассказал мне о моей проблеме примерно за 5 секунд. Неизвестно, сколько времени я бы потратил на отладку в противном случае. - person Bill the Lizard; 15.12.2008

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

Из исходного вопроса:

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.
person Martin York    schedule 15.12.2008
comment
что подразумевается под точкой последовательности? - person yesraaj; 15.12.2008
comment
en.wikipedia.org/wiki/Sequence_point - person Martin York; 15.12.2008
comment
Ох ... это мерзко, тем более, что я видел ту точную структуру, рекомендованную в Java - person Tom; 15.12.2008
comment
Обратите внимание, что некоторые компиляторы действительно определяют поведение в этой ситуации. В VC ++ 2005+, например, если a является изменчивым, необходимые барьеры памяти устанавливаются для предотвращения переупорядочения инструкций, чтобы сработала блокировка с двойной проверкой. - person Eclipse; 23.06.2009
comment
Мартин Йорк: ‹i› // (c) гарантированно произойдет после (a) и (b) ‹/i› Не так ли? По общему признанию, в этом конкретном примере единственный сценарий, в котором это могло иметь значение, был бы, если бы 'i' был изменчивой переменной, сопоставленной с аппаратным регистром, а [i] (старое значение 'i') было бы присвоено ей псевдонимом, но есть ли какие-либо гарантировать, что приращение произойдет до точки последовательности? - person supercat; 15.08.2010
comment
@supercat: Да, это гарантировано. Обе стороны = должны быть оценены, прежде чем его можно будет оценить. - person Martin York; 12.10.2010
comment
@Martin York: выражение i = i ++; обычно приводится как пример неопределенного поведения в C (я знаю, что поведение определено в некоторых других языках, таких как Java). Если побочные эффекты приращения не гарантированно исчезнут к тому времени, когда an i = i ++; происходит присвоение, почему они должны быть гарантированно завершены к моменту a [i] = i ++; имеет место? - person supercat; 12.10.2010
comment
@supercat: Хорошо, я понимаю, откуда вы. Вы правы, эффект от оператора ++ (на i) не гарантируется к этому моменту. - person Martin York; 12.10.2010
comment
a[i] = i++; в любом случае является неопределенным поведением. Компилятор может делать больше, чем просто переупорядочивать его, он может игнорировать это или делать что-то с носом. i модифицируется на RHS, а на LHS он читается иначе, чем с целью определения записываемого значения. Игра окончена (если i не является типом класса с operator++ перегрузкой, тогда любые модификации объекта внутри этой перегрузки надежно обертываются в точках последовательности, и это просто неуказанный порядок, а не UB). - person Steve Jessop; 07.06.2011
comment
@Steve: Совершенно верно. Я просто пытался объяснить, почему он должен быть неопределенным (и некоторые возможные объяснения реализации). - person Martin York; 07.06.2011
comment
@Martin: не уверен, что согласен с тем, что это объясняет, почему он должен быть неопределенным. Бывают и другие ситуации, в которых порядок операций не указан без вызова UB. Например, в случае, о котором я упоминал, где i - это тип класса, который имитирует целое число с подходящими operator++(int) и operator int(), ваши два варианта "либо / или" все еще возможны, но нет UB. Тем не менее, возможно, говоря о неопределенном поведении, спрашивающий имеет в виду любую ситуацию, в которой поведение не полностью закреплено, в отличие от стандартного определения UB. - person Steve Jessop; 07.06.2011
comment
Ваш пример блокировки с двойной проверкой порочен. ಥ_ಥ - person Brian Gordon; 25.09.2011
comment
Привет, я бродил, какие еще проблемы для второй версии реализации сиглтона. Вроде идеально. - person Joey.Z; 20.09.2013
comment
@zoujyjs: Если вы хотите создать синглтон с указателями, вы делаете это неправильно. См. шаблон проектирования C ++ Singleton - person Martin York; 20.09.2013
comment
Да, я читал об этом также в Эффективный C ++, пункт 04. И это гарантированно поточно-ориентированное после c ++ 0x. Но почему не указатели? А если нет, что если я просто верну разыменование указателей? Поскольку синглтон должен присутствовать на протяжении всей жизни процесса, мне не нужно было уничтожать его с помощью явного указателя. - person Joey.Z; 20.09.2013
comment
В частности, я хочу знать, считается ли приведенный выше код достаточно хорошей реализацией DCLP. - person Joey.Z; 20.09.2013
comment
@zoujyjs: Нет, это не хорошо в C ++ 03. В C ++ есть проблемы с потоковой передачей и блокировкой с двойной проверкой, что делает невозможным правильное выполнение; на нем есть знаменитая статья Саттера. Я считаю, что это исправлено для C ++ 11, но вам нужно будет использовать новые функции, не описанные здесь. - person Martin York; 20.09.2013
comment
@zoujyjs: Вот он: C ++ и «Опасности дважды проверенного запирания», собственно, Мейерс и Александреску. - person Martin York; 20.09.2013

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

person Daniel Earwicker    schedule 15.12.2008
comment
Сделал это раньше, но я не понимаю, как это не определено. Совершенно очевидно, что вы делаете бесконечную рекурсию запоздало. - person Robert Gould; 15.12.2008
comment
Проблема в том, что компилятор не может проверить ваш код и точно решить, будет ли он страдать от бесконечной рекурсии или нет. Это пример проблемы с остановкой. См .: stackoverflow.com/ questions / 235984 / - person Daniel Earwicker; 15.12.2008
comment
Да, это определенно проблема - person Robert Gould; 16.12.2008
comment
это привело к сбою моей системы из-за подкачки, вызванной слишком маленьким объемом памяти. - person Johannes Schaub - litb; 28.12.2008
comment
Константы препроцессора, которые не помещаются в int, также являются временем компиляции. - person Joshua; 18.08.2010

Назначение константе после удаления constness с помощью const_cast<>:

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

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

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

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

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

person Constantin    schedule 15.12.2008

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

int i =1;
i = ++i;

// Undefined. Assignment to 'i' twice in the same expression.
person Martin York    schedule 15.12.2008
comment
Фактически хотя бы один раз между двумя точками последовательности. - person Prasoon Saurav; 12.08.2010
comment
@Prasoon: я думаю, вы имели в виду: максимум один раз между двумя точками последовательности. :-) - person Nawaz; 07.06.2011

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

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

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

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

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

person RandomNickName42    schedule 19.03.2011
comment
Если вы достигнете этих пределов, у вас будет больше проблем, чем неопределенное поведение. - person new123456; 22.04.2011
comment
Вы можете ЛЕГКО превысить 65535 байт в объекте, таком как STD :: vector - person Demi; 13.09.2013

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

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

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

7.21.2.1 Функция memcpy

Синопсис

1 / #include void * memcpy (void * restrict s1, const void * restrict s2, size_t n);

Описание

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

7.21.2.2 Функция memmove

Синопсис

1 # включить 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.

person John Dibling    schedule 26.06.2012

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

person JaredPar    schedule 15.12.2008
comment
Разве не для этого есть ‹cstdint›? Он определяет такие типы, как uint16_6 и так далее. - person Jasper Bekkers; 15.12.2008
comment
Да, но размер большинства типов, скажем, длинных, точно не определен. - person JaredPar; 15.12.2008
comment
также cstdint еще не является частью текущего стандарта C ++. см. boost / stdint.hpp для в настоящее время переносимого решения. - person Evan Teran; 15.12.2008
comment
Это не неопределенное поведение. В стандарте говорится, что соответствующая платформа определяет размеры, а не стандарт, определяющий их. - person Daniel Earwicker; 15.12.2008
comment
Также не то, что стандарт не определяет, сколько 1 байт. Это как минимум 8 бит, но все, что разрешено выше, поэтому байт C ++ не обязательно равен байту реальной жизни / в любом случае, я голосую за вас, поскольку это не заслуживает отрицательного голосования. - person Sebastian Mach; 02.02.2010
comment
@JaredPar: Это не совсем так. long - это как минимум 32 бита. . - person John Dibling; 26.06.2012
comment
@JohnDibling Я просмотрел этот пост и связанные со ссылками, но не смог найти точного источника, поскольку это 32 бита. Один пользователь заявил об этом, но не было ссылки на спецификацию, которая сделала бы - person JaredPar; 29.06.2012
comment
@JaredPar: это сложный пост с множеством обсуждений, поэтому я подвел итог здесь. Суть в следующем: 5. Чтобы представить -2147483647 и +2147483647 в двоичном формате, вам нужно 32 бита. - person John Dibling; 29.06.2012
comment
По аналогичным причинам int составляет не менее 16 бит, а long long - не менее 64 (добавлено в стандарт в C ++ 11). - person David Stone; 21.02.2013

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

person yesraaj    schedule 22.12.2009