Неопределенное, неопределенное и определяемое реализацией поведение

530

Что такое неопределенное поведение в C и C ++? Как насчет неопределенного поведения и поведения, определенного реализацией? В чем разница между ними?

Zolomon
источник
1
Я был почти уверен, что мы сделали это раньше, но я не могу найти это. Смотрите также: stackoverflow.com/questions/2301372/…
dmckee --- котенок экс-модератора
1
Вот интересная дискуссия (раздел «Приложение L и неопределенное поведение»).
Оуэн

Ответы:

407

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

Давайте посмотрим на классический пример:

#include <iostream>

int main()
{
    char* p = "hello!\n";   // yes I know, deprecated conversion
    p[0] = 'y';
    p[5] = 'w';
    std::cout << p;
}

Переменная pуказывает на строковый литерал "hello!\n", и два нижеприведенных назначения пытаются изменить этот строковый литерал. Что делает эта программа? Согласно пункту 11 раздела 2.14.5 стандарта C ++, он вызывает неопределенное поведение :

Эффект попытки изменить строковый литерал не определен.

Я слышу, как люди кричат: «Но подождите, я могу без проблем скомпилировать и получить вывод yellow» или «Что вы подразумеваете под неопределенным, строковые литералы хранятся в постоянной памяти, поэтому первая попытка назначения приводит к дампу ядра». Это как раз проблема с неопределенным поведением. По сути, стандарт позволяет всему происходить, когда вы вызываете неопределенное поведение (даже носовые демоны). Если есть «правильное» поведение в соответствии с вашей ментальной моделью языка, эта модель просто неверна; Стандарт C ++ имеет единственный голос, точка.

Другие примеры неопределенного поведения включают доступ к массиву за его пределами, разыменование нулевого указателя , доступ к объектам после истечения срока их жизни или запись предположительно умных выражений типа i++ + ++i.

В разделе 1.9 стандарта C ++ также упоминаются два менее опасных брата неопределенного поведения: неопределенное поведение и поведение, определяемое реализацией :

Семантические описания в этом международном стандарте определяют параметризованную недетерминированную абстрактную машину.

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

Некоторые другие аспекты и операции абстрактной машины описаны в этом международном стандарте как неопределенные (например, порядок вычисления аргументов функции). Там, где это возможно, этот международный стандарт определяет набор допустимого поведения. Они определяют недетерминированные аспекты абстрактной машины.

Некоторые другие операции описаны в этом международном стандарте как неопределенные (например, эффект разыменования нулевого указателя). [ Примечание : этот международный стандарт не предъявляет никаких требований к поведению программ, которые содержат неопределенное поведение. - конец примечания ]

В частности, в разделе 1.3.24 говорится:

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

Что вы можете сделать, чтобы избежать неожиданного поведения? По сути, вы должны читать хорошие книги по С ++ от авторов, которые знают, о чем они говорят. Винт интернет-учебники. Винт Буллшильдт.

fredoverflow
источник
6
Это странный факт, возникший в результате слияния, что этот ответ охватывает только C ++, но теги этого вопроса включают C. C имеет другое понятие «неопределенное поведение»: ему все равно потребуется реализация, чтобы выдавать диагностические сообщения, даже если поведение также указано для быть неопределенным для определенных нарушений правил (нарушений ограничений).
Йоханнес Шауб -
8
@Benoit Это неопределенное поведение, потому что стандарт говорит, что это неопределенное поведение, точка. В некоторых системах действительно строковые литералы хранятся в текстовом сегменте, доступном только для чтения, и программа завершится сбоем, если вы попытаетесь изменить строковый литерал. В других системах строковый литерал действительно будет меняться. Стандарт не предписывает, что должно произойти. Вот что значит неопределенное поведение.
fredoverflow
5
@FredOverflow, Почему хороший компилятор позволяет нам компилировать код, который дает неопределенное поведение? Именно то , что хорошо может компиляции такого кода отдавания? Почему все хорошие компиляторы не дают нам огромный красный предупреждающий знак, когда мы пытаемся скомпилировать код, который дает неопределенное поведение?
Пейсер
14
@Pacerier Есть некоторые вещи, которые нельзя проверить во время компиляции. Например, не всегда возможно гарантировать, что нулевой указатель никогда не разыменовывается, но это не определено.
Тим Сегин
4
@Celeritas, неопределенное поведение может быть недетерминированным. Например, невозможно заранее знать, каким будет содержимое неинициализированной памяти, например. int f(){int a; return a;}: значение aможет меняться между вызовами функций.
Марк
97

Ну, это в основном прямая копия-вставка из стандартного

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

Пример 2 Примером поведения, определяемого реализацией, является распространение старшего бита, когда целое число со знаком сдвигается вправо.

3.4.3 1 неопределенное поведение при использовании непереносимой или ошибочной программной конструкции или ошибочных данных, для которых настоящий международный стандарт не предъявляет никаких требований

2 ПРИМЕЧАНИЕ Возможное неопределенное поведение варьируется от полного игнорирования ситуации с непредсказуемыми результатами до поведения во время перевода или выполнения программы документированным образом, характерным для среды (с выдачей диагностического сообщения или без него), до прекращения перевода или выполнения (с выдача диагностического сообщения).

3 ПРИМЕР Примером неопределенного поведения является поведение при целочисленном переполнении.

3.4.4 1 неуказанное поведение использование неуказанного значения или другое поведение, когда этот международный стандарт предоставляет две или более возможности и не предъявляет никаких дополнительных требований, которые выбираются в любом случае

2 ПРИМЕР Примером неуказанного поведения является порядок, в котором оцениваются аргументы функции.

Муравей
источник
3
В чем разница между определенным реализацией и неуказанным поведением?
Золомон
26
@Zolomon: Точно так же, как он говорит: в основном то же самое, за исключением того, что в случае реализации, определенной, требуется выполнение для документирования (для гарантии) того, что именно произойдет, в то время как в случае неопределенности реализация не требуется для документирования. или что-нибудь гарантировать.
AnT
1
@Zolomon: это отражается на разнице между 3.4.1 и 2.4.4.
ВОО
8
@Celeritas: Гиперсовременные компиляторы могут добиться большего успеха. Учитывая, int foo(int x) { if (x >= 0) launch_missiles(); return x << 1; }что компилятор может определить, что, поскольку все средства вызова функции, которая не запускает ракеты, вызывают неопределенное поведение, он может сделать вызов launch_missiles()безусловным.
суперкат
2
@northerner Как говорится в цитате, неопределенное поведение обычно ограничено ограниченным набором возможных вариантов поведения. В некоторых случаях вы можете даже прийти к выводу, что все эти возможности являются приемлемыми в данном контексте, и в этом случае неопределенное поведение вообще не является проблемой. Неопределенное поведение абсолютно неограниченно (например, «программа может решить отформатировать ваш жесткий диск»). Неопределенное поведение всегда является проблемой.
AnT
60

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

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

неопределенное поведение
Вы делаете что-то не так. Например, у вас есть очень большое значение в, intкоторое не вписывается char. Как вы вкладываете это значение char? на самом деле нет пути! Может произойти все что угодно, но самым разумным будет взять первый байт этого целого и вставить его char. Это просто неправильно делать это, чтобы назначить первый байт, но это то, что происходит под капотом.

неопределенное поведение
Какая функция из этих двух выполняется первой?

void fun(int n, int m);

int fun1()
{
  cout << "fun1";
  return 1;
}
int fun2()
{
  cout << "fun2";
  return 2;
}
...
fun(fun1(), fun2()); // which one is executed first?

Язык не определяет оценку, слева направо или справа налево! Таким образом, неопределенное поведение может привести или не привести к неопределенному поведению, но, безусловно, ваша программа не должна вызывать неопределенное поведение.


@eSKay Я думаю, что ваш вопрос стоит отредактировать ответ, чтобы уточнить больше :)

для fun(fun1(), fun2());не поведение «определяется реализацией»? Компилятор должен выбрать один или другой курс, в конце концов?

Различие между реализацией, определенной и неуказанной, состоит в том, что компилятор должен выбирать поведение в первом случае, но это не обязательно во втором случае. Например, реализация должна иметь одно и только одно определение sizeof(int). Таким образом, нельзя сказать, что sizeof(int)4 для какой-то части программы и 8 для других. В отличие от неопределенного поведения, когда компилятор может сказать «ОК», я собираюсь оценить эти аргументы слева направо, а аргументы следующей функции - справа налево. Это может происходить в одной и той же программе, поэтому она называется неопределенной . На самом деле, C ++ можно было бы сделать проще, если бы были указаны некоторые неуказанные поведения. Посмотрите здесь на ответ доктора Страуструпа для этого :

Утверждается, что разница между тем, что может быть создано, предоставляя компилятору эту свободу, и требуя "обычной оценки слева направо", может быть значительной. Я не уверен, но с бесчисленными компиляторами, «пользующимися свободой», и некоторыми людьми, страстно защищающими эту свободу, изменение будет трудным и может занять десятилетия, чтобы проникнуть в далекие уголки миров C и C ++. Я разочарован тем, что не все компиляторы предостерегают от такого кода, как ++ i + i ++. Точно так же порядок оценки аргументов не определен.

В IMO слишком много «вещей», оставленных неопределенными, неуказанными, определяемыми реализацией и т. Д. Однако, это легко сказать и даже привести примеры, но трудно исправить. Следует также отметить, что не так уж и сложно избежать большинства проблем и создать переносимый код.

арак
источник
1
для fun(fun1(), fun2());не поведение "implementation defined"? Компилятор должен выбрать один или другой курс, в конце концов?
Лазер
1
@AraK: спасибо за объяснение. Теперь я это понимаю. Кстати, "I am gonna evaluate these arguments left-to-right and the next function's arguments are evaluated right-to-left"я понимаю, что это canслучилось. Действительно ли это с компиляторами, которые мы используем в наши дни?
Lazer
1
@eSKay Вы должны спросить об этом гуру, который запачкал свои руки многими компиляторами :) AFAIK VC всегда оценивает аргументы справа налево.
AraK
4
@Lazer: это определенно может произойти. Простой сценарий: foo (bar, boz ()) и foo (boz (), bar), где bar - это int, а boz () - функция, возвращающая int. Предположим ЦП, где параметры должны передаваться в регистрах R0-R1. Результаты функции возвращаются в R0; функции могут мусор R1. Оценка «bar» перед «boz ()» потребует сохранения копии bar где-нибудь еще до вызова boz () и затем загрузки этой сохраненной копии. Оценка «bar» после «boz ()» позволит избежать сохранения и повторного извлечения памяти и является оптимизацией, которую могут выполнять многие компиляторы независимо от их порядка в списке аргументов.
суперкат
6
Я не знаю о C ++, но стандарт C говорит, что преобразование int в тип char определяется реализацией или даже хорошо определяется (в зависимости от фактических значений и подписи типов). См. C99 §6.3.1.3 (без изменений в C11).
Николай Рюэ
27

Из официального документа с обоснованием

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

Неуказанное поведение дает разработчику некоторую свободу в переводе программ. Эта широта не распространяется на то, что не удалось перевести программу.

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

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

Йоханнес Шауб - Литб
источник
3
Сверхсовременные авторы компиляторов также рассматривают «неопределенное поведение» как предоставление лицензии авторам компиляторов допущения, что программы никогда не получат входные данные, которые могут привести к неопределенному поведению, и произвольного изменения всех аспектов поведения программ при получении таких входных данных.
суперкат
2
Еще один момент, который я только что заметил: C89 не использовал термин «расширение» для описания функций, которые были гарантированы в некоторых реализациях, но не в других. Авторы C89 признали, что большинство текущих реализаций будут обрабатывать арифметику со знаком и арифметику без знака одинаково, за исключением случаев, когда результаты используются определенным образом, и такая обработка применяется даже в случае переполнения со знаком; однако они не перечислили это как общее расширение в Приложении J2, что наводит меня на мысль, что они рассматривают это как естественное положение дел, а не как продолжение.
суперкат
10

Неопределенное поведение против неуказанного поведения имеет краткое описание этого.

Их окончательное резюме:

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

Андерс Абель
источник
1
Существует два вида компиляторов: те, которые, если явно не задокументированы иным образом, интерпретируют большинство форм неопределенного поведения стандарта как отступление от характерных поведений, задокументированных базовой средой, и те, которые по умолчанию только с пользой раскрывают поведения, которые стандарт характеризует как реализации. При использовании компиляторов первого типа многие вещи первого типа могут быть выполнены эффективно и безопасно с использованием UB. Компиляторы для второго типа будут подходить для таких задач, только если они предоставляют опции, гарантирующие поведение в таких случаях.
суперкат
8

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

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

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

Например, учитывая следующий код:

int scaled_velocity(int v, unsigned char pow)
{
  if (v > 250)
    v = 250;
  if (v < -250)
    v = -250;
  return v << pow;
}

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

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

Supercat
источник
Но обработка неопределенного поведения хорошим способом не бесплатна. Причиной того, что современные компиляторы демонстрируют такое странное поведение в некоторых случаях UB, является то, что они неустанно оптимизируют, и для того, чтобы сделать лучшую работу в этом, они должны быть в состоянии предположить, что UB никогда не возникает.
Том Свирли
Но тот факт, что <<UB на отрицательных числах - маленькая неприятная ловушка, и я рад, что мне об этом напомнили!
Том Свирли
1
@TomSwirly: К сожалению, авторы компилятора не заботятся о том, что предоставление свободных поведенческих гарантий сверх тех, которые предусмотрены Стандартом, часто может привести к значительному увеличению скорости по сравнению с требованием, чтобы код любой ценой избегал чего-либо, не определенного Стандартом. Если программисту все равно, i+j>kвыдаст ли 1 или 0 в случаях, когда сложение переполняется, при условии, что у него нет других побочных эффектов , компилятор может выполнить некоторые значительные оптимизации, которые были бы невозможны, если бы программист написал код как (int)((unsigned)i+j) > k.
суперкат
1
@TomSwirly: Для них, если компилятор X может взять строго соответствующую программу для выполнения некоторой задачи T и получить исполняемый файл, который на 5% эффективнее, чем компилятор Y с той же программой, это означает, что X лучше, даже если Y может генерировать код, который выполнял ту же задачу в три раза эффективнее, учитывая программу, которая использует поведение, которое Y гарантирует, но X не делает.
суперкат
6

Стандарт C ++ n3337 § 1.3.10 Поведение, определяемое реализацией

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

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


Стандарт C ++ n3337 § 1.3.24 неопределенное поведение

к поведению, для которого настоящий международный стандарт не предъявляет никаких требований [Примечание. Неопределенное поведение может ожидаться, когда в этом международном стандарте отсутствует какое-либо явное определение поведения или когда программа использует ошибочную конструкцию или ошибочные данные. Допустимое неопределенное поведение варьируется от полного игнорирования ситуации с непредсказуемыми результатами до поведения во время перевода или выполнения программы документированным образом, характерным для среды (с выдачей диагностического сообщения или без него), до прекращения перевода или выполнения (с выдачей диагностического сообщения). Многие ошибочные программные конструкции не порождают неопределенного поведения; они должны быть диагностированы. - конец примечания]

Когда программа встречает конструкцию, которая не определена в соответствии со Стандартом C ++, ей разрешается делать все, что она хочет (возможно, отправить мне электронное письмо или отправить вам электронное письмо, или, возможно, полностью игнорировать код).


Стандарт C ++ n3337 § 1.3.25 неопределенное поведение

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

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

4pie0
источник
6

Реализация определена

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

Неопределенные -

То же, что определяется реализацией, но не задокументировано

Undefined-

Все может случиться, позаботься об этом.

Сурадж К Томас
источник
2
Я думаю, что важно отметить, что практическое значение «неопределенного» изменилось за последние несколько лет. Раньше считалось , что с учетом uint32_t s;, оценки , 1u<<sкогда sбудет 33 можно ожидать , что, может быть , выход 0 или , может быть , выход 2, но не делать ничего дурацкие. Однако более новые компиляторы, оценивающие, 1u<<sмогут заставить компилятор определить, что, поскольку он sдолжен был быть меньше 32 до этого, любой код до или после этого выражения, который был бы релевантным, если бы sон был 32 или больше, мог быть пропущен.
суперкат