Что такое неопределенное поведение в C и C ++? Как насчет неопределенного поведения и поведения, определенного реализацией? В чем разница между ними?
Что такое неопределенное поведение в C и C ++? Как насчет неопределенного поведения и поведения, определенного реализацией? В чем разница между ними?
Ответы:
Неопределенное поведение - один из тех аспектов языка 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 говорится:
Допустимое неопределенное поведение варьируется от полного игнорирования ситуации с непредсказуемыми результатами до поведения во время перевода или выполнения программы документированным образом, характерным для среды (с выдачей диагностического сообщения или без него), до прекращения перевода или выполнения (с выдачей диагностического сообщения).
Что вы можете сделать, чтобы избежать неожиданного поведения? По сути, вы должны читать хорошие книги по С ++ от авторов, которые знают, о чем они говорят. Винт интернет-учебники. Винт Буллшильдт.
int f(){int a; return a;}
: значение a
может меняться между вызовами функций.
Ну, это в основном прямая копия-вставка из стандартного
3.4.1 1 поведение, определяемое реализацией, неопределенное поведение, где каждая реализация документирует, как сделан выбор
Пример 2 Примером поведения, определяемого реализацией, является распространение старшего бита, когда целое число со знаком сдвигается вправо.
3.4.3 1 неопределенное поведение при использовании непереносимой или ошибочной программной конструкции или ошибочных данных, для которых настоящий международный стандарт не предъявляет никаких требований
2 ПРИМЕЧАНИЕ Возможное неопределенное поведение варьируется от полного игнорирования ситуации с непредсказуемыми результатами до поведения во время перевода или выполнения программы документированным образом, характерным для среды (с выдачей диагностического сообщения или без него), до прекращения перевода или выполнения (с выдача диагностического сообщения).
3 ПРИМЕР Примером неопределенного поведения является поведение при целочисленном переполнении.
3.4.4 1 неуказанное поведение использование неуказанного значения или другое поведение, когда этот международный стандарт предоставляет две или более возможности и не предъявляет никаких дополнительных требований, которые выбираются в любом случае
2 ПРИМЕР Примером неуказанного поведения является порядок, в котором оцениваются аргументы функции.
int foo(int x) { if (x >= 0) launch_missiles(); return x << 1; }
что компилятор может определить, что, поскольку все средства вызова функции, которая не запускает ракеты, вызывают неопределенное поведение, он может сделать вызов launch_missiles()
безусловным.
Возможно, легкая формулировка может быть легче для понимания, чем строгое определение стандартов.
Поведение, определяемое реализацией
Язык говорит, что у нас есть типы данных. Поставщики компиляторов указывают, какие размеры они будут использовать, и предоставляют документацию о том, что они сделали.
неопределенное поведение
Вы делаете что-то не так. Например, у вас есть очень большое значение в, 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 слишком много «вещей», оставленных неопределенными, неуказанными, определяемыми реализацией и т. Д. Однако, это легко сказать и даже привести примеры, но трудно исправить. Следует также отметить, что не так уж и сложно избежать большинства проблем и создать переносимый код.
fun(fun1(), fun2());
не поведение "implementation defined"
? Компилятор должен выбрать один или другой курс, в конце концов?
"I am gonna evaluate these arguments left-to-right and the next function's arguments are evaluated right-to-left"
я понимаю, что это can
случилось. Действительно ли это с компиляторами, которые мы используем в наши дни?
Из официального документа с обоснованием
Термины неопределенное поведение, неопределенное поведение и поведение, определяемое реализацией , используются для категоризации результатов написания программ, свойства которых Стандарт не описывает или не может полностью описать. Цель принятия этой категоризации состоит в том, чтобы обеспечить определенное разнообразие среди реализаций, которое позволяет качеству реализации быть активной силой на рынке, а также разрешить определенные популярные расширения, не удаляя кэш соответствия стандарта. В Приложении F к Стандарту перечислены те виды поведения, которые подпадают под одну из этих трех категорий.
Неуказанное поведение дает разработчику некоторую свободу в переводе программ. Эта широта не распространяется на то, что не удалось перевести программу.
Неопределенное поведение дает разработчику лицензию на не обнаружение определенных программных ошибок, которые трудно диагностировать. Он также определяет области возможного соответствующего расширения языка: разработчик может расширить язык, предоставив определение официально неопределенного поведения.
Поведение, определяемое реализацией, дает разработчику свободу выбора подходящего подхода, но требует, чтобы этот выбор был объяснен пользователю. Поведения, обозначенные как определенные реализацией, - это, как правило, те, в которых пользователь может принимать значимые решения по кодированию на основе определения реализации. Разработчики должны учитывать этот критерий при принятии решения о том, насколько обширным должно быть определение реализации. Как и в случае с неопределенным поведением, простой отказ от перевода источника, содержащего поведение, определяемое реализацией, не является адекватным ответом.
Неопределенное поведение против неуказанного поведения имеет краткое описание этого.
Их окончательное резюме:
Подводя итог, можно сказать, что вам не следует беспокоиться о неопределенном поведении, если только ваше программное обеспечение не должно быть переносимым. И наоборот, неопределенное поведение всегда нежелательно и никогда не должно происходить.
Исторически сложилось так, что определяемое реализацией поведение и неопределенное поведение представляли ситуации, в которых авторы стандарта ожидали, что люди, пишущие качественные реализации, будут использовать суждение, чтобы решить, какие поведенческие гарантии, если таковые имеются, будут полезны для программ в предполагаемой области приложения, работающей на предполагаемые цели. Потребности высокопроизводительного кода для обработки чисел сильно отличаются от потребностей низкоуровневого системного кода, и как 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 как подразумевая, что авторы компилятора должны свободно игнорировать это.
<<
UB на отрицательных числах - маленькая неприятная ловушка, и я рад, что мне об этом напомнили!
i+j>k
выдаст ли 1 или 0 в случаях, когда сложение переполняется, при условии, что у него нет других побочных эффектов , компилятор может выполнить некоторые значительные оптимизации, которые были бы невозможны, если бы программист написал код как (int)((unsigned)i+j) > k
.
Стандарт C ++ n3337 § 1.3.10 Поведение, определяемое реализацией
поведение, для правильно сформированной программы построить и исправить данные, которые зависят от реализации и что каждая реализация документирует
Иногда C ++ Standard не навязывает определенное поведение некоторым конструкциям, а вместо этого говорит, что конкретное, четко определенное поведение должно быть выбрано и описано конкретной реализацией (версией библиотеки). Таким образом, пользователь все еще может точно знать, как будет вести себя программа, хотя Standard не описывает этого.
Стандарт C ++ n3337 § 1.3.24 неопределенное поведение
к поведению, для которого настоящий международный стандарт не предъявляет никаких требований [Примечание. Неопределенное поведение может ожидаться, когда в этом международном стандарте отсутствует какое-либо явное определение поведения или когда программа использует ошибочную конструкцию или ошибочные данные. Допустимое неопределенное поведение варьируется от полного игнорирования ситуации с непредсказуемыми результатами до поведения во время перевода или выполнения программы документированным образом, характерным для среды (с выдачей диагностического сообщения или без него), до прекращения перевода или выполнения (с выдачей диагностического сообщения). Многие ошибочные программные конструкции не порождают неопределенного поведения; они должны быть диагностированы. - конец примечания]
Когда программа встречает конструкцию, которая не определена в соответствии со Стандартом C ++, ей разрешается делать все, что она хочет (возможно, отправить мне электронное письмо или отправить вам электронное письмо, или, возможно, полностью игнорировать код).
Стандарт C ++ n3337 § 1.3.25 неопределенное поведение
поведение, для правильно сформированной программы и правильных данных, которые зависят от реализации [Примечание: реализация не обязана документировать, какое поведение происходит. Диапазон возможных вариантов поведения обычно определяется этим международным стандартом. - конец примечания]
Стандарт C ++ не навязывает определенное поведение некоторым конструкциям, но вместо этого говорит, что конкретное, четко определенное поведение должно быть выбрано ( бот не описан ) определенной реализацией (версия библиотеки). Таким образом, в случае, когда описание не было предоставлено, пользователю может быть трудно точно знать, как будет вести себя программа.
Реализация определена
Разработчики желают, должны быть хорошо документированы, стандарт дает выбор, но обязательно компилируется
Неопределенные -
То же, что определяется реализацией, но не задокументировано
Undefined-
Все может случиться, позаботься об этом.
uint32_t s;
, оценки , 1u<<s
когда s
будет 33 можно ожидать , что, может быть , выход 0 или , может быть , выход 2, но не делать ничего дурацкие. Однако более новые компиляторы, оценивающие, 1u<<s
могут заставить компилятор определить, что, поскольку он s
должен был быть меньше 32 до этого, любой код до или после этого выражения, который был бы релевантным, если бы s
он был 32 или больше, мог быть пропущен.