Что такое неопределенное поведение в C и C ++? Как насчет неопределенного поведения и поведения, определенного реализацией? В чем разница между ними?
530
Что такое неопределенное поведение в C и C ++? Как насчет неопределенного поведения и поведения, определенного реализацией? В чем разница между ними?
Ответы:
Неопределенное поведение - один из тех аспектов языка C и C ++, который может удивлять программистов, пришедших из других языков (другие языки пытаются скрыть это лучше). По сути, можно писать программы на C ++, которые не ведут себя предсказуемо, даже если многие компиляторы C ++ не будут сообщать о каких-либо ошибках в программе!
Давайте посмотрим на классический пример:
Переменная
p
указывает на строковый литерал"hello!\n"
, и два нижеприведенных назначения пытаются изменить этот строковый литерал. Что делает эта программа? Согласно пункту 11 раздела 2.14.5 стандарта C ++, он вызывает неопределенное поведение :Я слышу, как люди кричат: «Но подождите, я могу без проблем скомпилировать и получить вывод
yellow
» или «Что вы подразумеваете под неопределенным, строковые литералы хранятся в постоянной памяти, поэтому первая попытка назначения приводит к дампу ядра». Это как раз проблема с неопределенным поведением. По сути, стандарт позволяет всему происходить, когда вы вызываете неопределенное поведение (даже носовые демоны). Если есть «правильное» поведение в соответствии с вашей ментальной моделью языка, эта модель просто неверна; Стандарт C ++ имеет единственный голос, точка.Другие примеры неопределенного поведения включают доступ к массиву за его пределами, разыменование нулевого указателя , доступ к объектам после истечения срока их жизни или запись предположительно умных выражений типа
i++ + ++i
.В разделе 1.9 стандарта C ++ также упоминаются два менее опасных брата неопределенного поведения: неопределенное поведение и поведение, определяемое реализацией :
В частности, в разделе 1.3.24 говорится:
Что вы можете сделать, чтобы избежать неожиданного поведения? По сути, вы должны читать хорошие книги по С ++ от авторов, которые знают, о чем они говорят. Винт интернет-учебники. Винт Буллшильдт.
источник
int f(){int a; return a;}
: значениеa
может меняться между вызовами функций.Ну, это в основном прямая копия-вставка из стандартного
источник
int foo(int x) { if (x >= 0) launch_missiles(); return x << 1; }
что компилятор может определить, что, поскольку все средства вызова функции, которая не запускает ракеты, вызывают неопределенное поведение, он может сделать вызовlaunch_missiles()
безусловным.Возможно, легкая формулировка может быть легче для понимания, чем строгое определение стандартов.
Поведение, определяемое реализацией
Язык говорит, что у нас есть типы данных. Поставщики компиляторов указывают, какие размеры они будут использовать, и предоставляют документацию о том, что они сделали.
неопределенное поведение
Вы делаете что-то не так. Например, у вас есть очень большое значение в,
int
которое не вписываетсяchar
. Как вы вкладываете это значениеchar
? на самом деле нет пути! Может произойти все что угодно, но самым разумным будет взять первый байт этого целого и вставить егоchar
. Это просто неправильно делать это, чтобы назначить первый байт, но это то, что происходит под капотом.неопределенное поведение
Какая функция из этих двух выполняется первой?
Язык не определяет оценку, слева направо или справа налево! Таким образом, неопределенное поведение может привести или не привести к неопределенному поведению, но, безусловно, ваша программа не должна вызывать неопределенное поведение.
@eSKay Я думаю, что ваш вопрос стоит отредактировать ответ, чтобы уточнить больше :)
Различие между реализацией, определенной и неуказанной, состоит в том, что компилятор должен выбирать поведение в первом случае, но это не обязательно во втором случае. Например, реализация должна иметь одно и только одно определение
sizeof(int)
. Таким образом, нельзя сказать, чтоsizeof(int)
4 для какой-то части программы и 8 для других. В отличие от неопределенного поведения, когда компилятор может сказать «ОК», я собираюсь оценить эти аргументы слева направо, а аргументы следующей функции - справа налево. Это может происходить в одной и той же программе, поэтому она называется неопределенной . На самом деле, C ++ можно было бы сделать проще, если бы были указаны некоторые неуказанные поведения. Посмотрите здесь на ответ доктора Страуструпа для этого :источник
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
случилось. Действительно ли это с компиляторами, которые мы используем в наши дни?Из официального документа с обоснованием
источник
Неопределенное поведение против неуказанного поведения имеет краткое описание этого.
Их окончательное резюме:
источник
Исторически сложилось так, что определяемое реализацией поведение и неопределенное поведение представляли ситуации, в которых авторы стандарта ожидали, что люди, пишущие качественные реализации, будут использовать суждение, чтобы решить, какие поведенческие гарантии, если таковые имеются, будут полезны для программ в предполагаемой области приложения, работающей на предполагаемые цели. Потребности высокопроизводительного кода для обработки чисел сильно отличаются от потребностей низкоуровневого системного кода, и как UB, так и IDB предоставляют разработчикам компиляторов гибкость для удовлетворения этих различных потребностей. Ни одна из категорий не требует, чтобы реализации вели себя так, как это полезно для какой-либо конкретной цели или даже для какой-либо цели. Качественные реализации, которые претендуют на то, что они подходят для конкретной цели, однако, должны вести себя в соответствии с такой цельютребует ли Стандарт этого или нет .
Единственное различие между поведением, определяемым реализацией, и поведением с неопределенным поведением заключается в том, что первое требует, чтобы реализации определяли и документировали согласованное поведение даже в тех случаях, когда ничего, что могла бы сделать реализация, не было бы полезно . Разграничительная черта между ними заключается не в том, было бы полезно для реализаций определять поведение (авторы компилятора должны определять полезные поведения, когда это целесообразно, требует ли Стандарт этого или нет), но могут ли быть реализации, где определение поведения будет одновременно дорогостоящим и бесполезно . Суждение о том, что такие реализации могут существовать, никоим образом не формирует и не формирует какого-либо суждения о полезности поддержки определенного поведения на других платформах.
К сожалению, с середины 1990-х годов авторы компиляторов начали интерпретировать отсутствие поведенческих мандатов как суждение о том, что поведенческие гарантии не стоят затрат даже в тех областях приложения, где они жизненно важны, и даже в системах, где они практически ничего не стоят. Вместо того, чтобы рассматривать UB как приглашение проявить разумное суждение, авторы компиляторов начали рассматривать его как предлог, чтобы не делать этого.
Например, учитывая следующий код:
реализация двойного дополнения не должна была бы затрачивать никаких усилий, чтобы трактовать выражение
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 или больше, мог быть пропущен.