Действительно ли" неопределенное поведение " позволяет *чему-либо* произойти? [дубликат]

этот вопрос уже есть ответ здесь:

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


потому что сообщества C и C++, как правило, делают такой акцент на непредсказуемость неопределенного поведения и идею, что компилятору разрешено заставьте программу делать буквально что-нибудь

но соответствующая цитата в стандарте C++ кажется:

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

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

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

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

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

EDIT: две небольшие пояснения:

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

9 ответов


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

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


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

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

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

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

#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 даст неопределенное поведение, и как ля следствие, программа должна напечатать "Привет!"безусловно, независимо какой характер был напечатан.


придирки: вы не процитировали стандарт.

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

толкование: ноты не нормативно - согласно директивам ISO/IEC Часть 2.

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

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

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


определение неопределенного поведения в каждом стандарте C и c++ заключается в том, что стандарт не накладывает никаких требований на то, что происходит.

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

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


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

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

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

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, все разрешено, включая просто не запуск проверок "is NULL". Я считаю, что подобные аргументы применимы к другому неопределенному поведению.


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

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

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

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

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

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

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

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


почему поведение не определено, но определено?

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

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

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

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


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

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

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

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

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

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


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

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

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


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

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

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

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

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

Edit: (в respons to OPs edit) Реализация Определенное поведение требовало бы последовательного порождения носовых демонов. Неопределенное поведение позволяет спорадически порождать носовых демонов.

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