Действительно ли "Undefined Поведение" действительно позволяет * что-либо * произойти?

EDIT: этот вопрос не был предназначен для обсуждения вопроса о (de) достоинствах поведения undefined, но такого рода, каким он стал. В любом случае этот поток о гипотетическом C-компиляторе без поведения undefined может представлять интерес для тех, кто считает, что это важный тема.


Классический апокрифический пример поведения undefined - это, конечно же, "носовые демоны"; физическая невозможность, независимо от того, что позволяют стандарты C и С++.

Поскольку сообщества C и С++ склонны уделять такое внимание непредсказуемости поведения undefined и мысли о том, что компилятору разрешено заставить программу буквально что-либо делать, когда встречается поведение undefined, я предположил что стандарт не налагает никаких ограничений на поведение поведения undefined.

Но релевантная цитата в стандарте С++ выглядит следующим образом:

[C++14: defns.undefined]: [..] Допустимое поведение undefined варьируется от полного игнорирования ситуации с непредсказуемыми результатами, ведения во время перевода или выполнения программы документированным образом, характерным для среды (с выдачей диагностического сообщения или без него), до прекращения перевода или выполнения (с выдачей диагностического сообщения). [..]

Это фактически задает небольшой набор возможных параметров:

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

Я предполагаю, что в большинстве случаев компиляторы предпочитают игнорировать поведение undefined; например, при чтении неинициализированной памяти, по-видимому, это будет анти-оптимизация для вставки любого кода для обеспечения согласованного поведения. Я полагаю, что незнакомые типы поведения undefined (например, путешествие во времени ") попадают под вторую категорию, но это требует, чтобы такое поведение должно быть документировано и" характерно для окружающей среды" (так что, как мне кажется, носовые демоны производятся только адскими компьютерами?).

Не понял ли я определение? Являются ли они примерами того, что могло бы представлять собой поведение undefined, а не полный список вариантов? Утверждается, что "что-то может случиться" означает просто неожиданный побочный эффект игнорирования ситуации?

РЕДАКТИРОВАТЬ: Две второстепенные точки уточнения:

  • Я думал, что это ясно из первоначального вопроса, и я думаю, что большинство людей это было, но я все равно его заклинаю: я понимаю, что "носовые демоны" - это язык в щеке.
  • Пожалуйста, не пишите (другой) ответ, поясняющий, что UB допускает оптимизацию компилятора на платформе, если вы не объясните, как он позволяет оптимизациям, которые поведение, определяемое реализацией, не позволяет.

Ответ 1

Да, это позволяет всему происходить. В записке просто приведены примеры. Определение довольно ясно:

Неопределенное поведение: поведение, к которому настоящий международный стандарт не предъявляет никаких требований.


Частая путаница:

Вы должны понимать, что "отсутствие требований" также означает, что реализация НЕ обязана оставлять поведение неопределенным или делать что-то странное/недетерминированное!

Реализация в стандарте C++ вполне допускает документирование некоторого вменяемого поведения и соответствующего поведения. 1 Итак, если ваш компилятор заявляет, что имеет дело с переполнением со знаком, логика (здравомыслие?) Будет диктовать, что вы можете положиться на это поведение на этом компиляторе. Только не ожидайте, что другой компилятор будет вести себя так же, если он не претендует на это.

1 Черт, это даже позволило документировать одно, а делать другое. Это было бы глупо, и это, вероятно, заставило бы вас выбросить его в мусорное ведро - зачем доверять компилятору, чья документация лежит на вас? - но это не противоречит стандарту C++.

Ответ 2

Одна из исторических целей Undefined Behavior заключалась в том, чтобы позволить, чтобы определенные действия могли иметь различные потенциально полезные эффекты на разных платформах. Например, в первые дни C, учитывая

int i=INT_MAX;
i++;
printf("%d",i);

некоторые компиляторы могут гарантировать, что код напечатает какое-то определенное значение (для машины с двумя дополнениями это обычно будет INT_MIN), в то время как другие будут гарантировать, что программа завершится без достижения printf. В зависимости от требований приложения, поведение может быть полезным. Оставив поведение Undefined, означало, что приложение, в котором аномальное завершение программы было приемлемым последствием переполнения, но при создании вывода, казалось бы, корректного, но неправильного, не было бы, можно было бы перекрыть проверку переполнения, если она будет запущена на платформе, которая надежно захватила бы ее, и приложение, где ненормальное завершение в случае переполнения было бы неприемлемым, но получение арифметически неверного вывода было бы, могло бы перекрыть проверку переполнения, если они запущены на платформе, где переполнение не было захвачено.

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

#include <stdio.h>

int main(void)
{
  int ch = getchar();
  if (ch < 74)
    printf("Hey there!");
  else
    printf("%d",ch*ch*ch*ch*ch);
}

гиперсовременный компилятор может заключить, что если ch равно 74 или выше, вычисление ch*ch*ch*ch*ch приведет к Undefined Поведение, а в качестве В результате программа должна напечатать "Эй, там!" безоговорочно о том, какой символ был напечатан.

Ответ 3

Nitpicking. Вы не указали стандарт.

Это источники, используемые для создания черновиков стандарта С++. Эти источники не следует рассматривать как публикацию ИСО, а также документы, созданные из них, если они официально не приняты рабочей группой С++ (ISO/IEC JTC1/SC22/WG21).

Интерпретация. Примечания не являются нормативными в соответствии с Директивой ISO/IEC Part 2.

Примечания и примеры, включенные в текст документа, должны использоваться только для предоставления дополнительной информации, предназначенной для содействия пониманию или использованию документа. Они не должны содержать требования ( "должны", см. 3.3.1 и таблицу H.1) или любую информацию, которая считается необходимой для использования документа, например. инструкции (обязательно, см. таблицу H.1), рекомендации ( "следует", см. 3.3.2 и таблицу H.2) или разрешение ( "может", см. таблицу H.3). Заметки могут быть написаны как утверждение факта.

Акцент мой. Это само по себе исключает "полный список вариантов". Однако приведение примеров относится к "дополнительной информации, предназначенной для содействия пониманию документа".

Имейте в виду, что "носовой демон" не должен восприниматься буквально, так же как использование воздушного шара для объяснения того, как вселенная расширяется, не имеет правды в физической реальности. Это иллюстрирует, что безрассудно обсуждать, что делать < undefined поведение должно, когда разрешено что-либо делать. Да, это означает, что в космическом пространстве нет реальной резиновой ленты.

Ответ 4

Определение поведения undefined в каждом стандарте C и С++ по существу таково, что стандарт не налагает никаких требований на то, что происходит.

Да, это означает, что любой результат разрешен. Но особых результатов не требуется и никаких результатов, которые НЕ должны произойти. Не имеет значения, есть ли у вас компилятор и библиотека, которые последовательно дают определенное поведение в ответ на конкретный экземпляр поведения undefined - такое поведение не требуется и может изменяться даже в будущем выпуске исправления вашего компилятора - и компилятор по-прежнему будет абсолютно корректен в соответствии с каждой версией стандартов C и С++.

Если ваша хост-система имеет аппаратную поддержку в виде соединения с зондами, вставленными в ваши ноздри, в пределах возможной вероятности, что возникновение поведения undefined приведет к нежелательным носовым эффектам.

Ответ 5

Я думал, что отвечу только на один из ваших вопросов, так как другие ответы отвечают на общий вопрос достаточно хорошо, но оставили это без внимания.

"Игнорирование ситуации. Да, в стандарте говорится, что это будет иметь" непредсказуемые результаты ", но это не то же самое, что и код компилятора, который, как я полагаю, был бы необходимым условием, знаете ли, носовым демоны)".

Ситуация, в которой можно было бы разумно ожидать, что носовые демоны произойдут с разумным компилятором, без компилятора, вставляющего ЛЮБОЙ код, будет следующим:

if(!spawn_of_satan)
    printf("Random debug value: %i\n", *x); // oops, null pointer deference
    nasal_angels();
else
    nasal_demons();

Компилятор, если он может доказать, что это * x является разыменованием нулевого указателя, совершенно определенно, как часть некоторой оптимизации, сказать "ОК, поэтому я вижу, что они разыменовали нулевой указатель в этой ветке if. Поэтому, как часть этой ветки, мне разрешено что-либо делать. Поэтому я могу оптимизировать это:"

if(!spawn_of_satan)
    nasal_demons();
else
    nasal_demons();

"И оттуда я могу оптимизировать это:"

nasal_demons();

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

EDIT: Один пример, который только что пришел из глубины моей памяти о таком случае, когда он полезен для оптимизации, вы часто проверяете указатель на то, что он является NULL (возможно, в встроенных вспомогательных функциях), даже после того, как он был разыменован и не изменив его. Оптимизирующий компилятор может видеть, что вы разыменовали его, и поэтому оптимизируйте все проверки "is NULL", поскольку, если вы разыменовали его и он имеет значение NULL, что-либо разрешено, в том числе просто не выполняется "NULL", чеки. Я считаю, что аналогичные аргументы применимы к другим поведениям undefined.

Ответ 6

Во-первых, важно отметить, что не только поведение пользовательской программы undefined, но и поведение компилятора, что - undefined. Аналогично, UB не встречается во время выполнения, это свойство исходного кода.

Для писателя-компилятора "поведение undefined" означает "вам не нужно учитывать эту ситуацию", или даже "вы не можете предположить, что исходный код никогда не произведет эту ситуацию". Компилятор может делать что угодно, преднамеренно или непреднамеренно, когда представлен UB, и по-прежнему является стандартным, поэтому да, если вы предоставили доступ к носу...

Тогда не всегда возможно узнать, имеет ли программа UB или нет. Пример:

int * ptr = calculateAddress();
int i = *ptr;

Зная, может ли это когда-либо быть UB или нет, потребуется знание всех возможных значений, возвращаемых calculateAddress(), что невозможно в общем случае (см. " Halting Проблема" ). Компилятор имеет два варианта:

  • Предположим, что ptr всегда будет иметь действительный адрес
  • вставить проверки выполнения, чтобы гарантировать определенное поведение.

Первый вариант создает быстрые программы и ставит бремя избежать нежелательных эффектов на программиста, а второй - более безопасный, но медленный код.

Стандарты C и С++ оставляют этот выбор открытым, и большинство компиляторов выбирают первый, тогда как Java, например, назначает второй.


Почему поведение не определено реализацией, но undefined?

Реализация, определенная означает (N4296, 1.9§2):

Некоторые аспекты и операции абстрактной машины описаны в этом Международном стандарте как (например, SizeOf (INT)). Они составляют параметры абстрактной машины. Каждая реализация должна включать документацию, описывающую ее характеристики и поведение в этих отношениях. Такая документация должна определять экземпляр абстрактной машины, которая соответствует этому реализация (именуемая "соответствующий экземпляр" ниже).

Акцент мой. Другими словами: компилятор-писатель должен точно документировать, как ведет себя машинный код, когда исходный код использует определенные для реализации функции.

Запись на случайный ненулевой недействительный указатель - одна из самых непредсказуемых вещей, которые вы можете сделать в программе, поэтому для этого потребуется также сократить время выполнения. Прежде чем мы имели MMU, вы могли уничтожить аппаратное обеспечение, написав неправильный адрес, который очень близок к носовым демонам; -)

Ответ 7

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

Если существует какое-то условие, которое должно выполняться, если необходимо применить оптимизацию, и это условие зависит от поведения undefined в коде, тогда компилятор может предположить, что он встретился, поскольку соответствующая программа не может зависеть на поведение undefined. Важно отметить, что компилятор не должен быть последовательным в этих предположениях. (что не относится к реализации, определяемому реализацией)

Итак, предположим, что ваш код содержит на самом деле надуманный пример, подобный приведенному ниже:

int bar = 0;
int foo = (undefined behavior of some kind);
if (foo) {
   f();
   bar = 1;
}
if (!foo) {
   g();
   bar = 1;
}
assert(1 == bar);

Компилятор может предположить, что! foo - true в первом блоке, а foo - true во втором и, таким образом, оптимизирует весь фрагмент кода. Теперь логически либо foo, либо! Foo должно быть истинным, и поэтому, глядя на код, вы разумно сможете предположить, что бар должен равняться 1 после запуска кода. Но поскольку компилятор оптимизирован таким образом, бару никогда не присваивается значение 1. И теперь это утверждение становится ложным, и программа завершается, что является поведением, которое не было бы, если бы foo не полагался на поведение undefined.

Теперь, возможно ли, чтобы компилятор действительно ввел совершенно новый код, если он видит поведение undefined? Если это позволит ему оптимизировать больше, абсолютно. Возможно, это произойдет часто? Наверное, нет, но вы никогда не сможете этого гарантировать, поэтому, исходя из предположения, что носовые демоны возможны, это единственный безопасный подход.

Ответ 8

Undefined поведение - это просто результат ситуации, которую авторы спецификации не предвидели.

Возьмите идею светофора. Красный означает остановку, желтый означает готовность к красному, а зеленый означает "идти". В этом примере люди, управляющие автомобилями, - это реализация спецификации.

Что произойдет, если включены как зеленые, так и красные? Ты останавливаешься, а потом уходишь? Вы ждете, пока красный не выключится, и он просто зеленый? Это случай, который спецификация не описала, и, как результат, все, что делают драйверы, это поведение undefined. Некоторые люди сделают одно, другое. Поскольку нет гарантии того, что произойдет, вы хотите избежать этой ситуации. То же самое касается кода.

Ответ 9

Undefined поведение позволяет компиляторам генерировать более быстрый код в некоторых случаях. Рассмотрим две разные архитектуры процессоров, которые ADD по-разному: Процессор A по существу отбрасывает бит переноса при переполнении, а процессор B генерирует ошибку. (Конечно, процессор C по своей сути генерирует Nasal Demons - его просто самый простой способ разрядить этот дополнительный бит энергии в наноботе с сопливым питанием...)

Если стандарту требовалось создать ошибку, то весь код, скомпилированный для процессора A, в основном был бы вынужден включать дополнительные инструкции, чтобы выполнить некоторую проверку для переполнения, и если это так, сгенерируйте ошибку. Это приведет к более медленному коду, даже если разработчик знает, что они только собираются в итоге добавить небольшие числа.

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

Кроме того, когда программист точно знает, что поведение undefined действительно будет иметь место в данной среде, они могут использовать эти знания для получения дополнительной производительности.

Если вы хотите, чтобы ваш код вел себя одинаково на всех платформах, вам нужно убедиться, что поведение < undefined когда-либо произошло, однако это может быть не ваша цель.

Изменить: (в ответ на редактирование OP) Реализация Определенное поведение потребовало бы последовательной генерации носовых демонов. undefined поведение допускает спорадическую генерацию носовых демонов.

В этом случае появляется преимущество, которое имеет поведение undefined по сравнению с конкретным поведением конкретной реализации. Учтите, что для предотвращения непоследовательного поведения в конкретной системе может потребоваться дополнительный код. В этих случаях поведение undefined позволяет увеличить скорость.