О неопределенном поведении

Как правило, UB считается чем-то, чего следует избегать, и сам текущий стандарт C содержит довольно много примеров в приложении J.

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

Рассмотрим следующее определение:

int a = INT_MAX + 1;

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

На мой взгляд, иногда UB — это просто способ стандарта C сказать мне: «Надеюсь, вы знаете, что делаете, потому что мы не можем дать никаких гарантий относительно того, что произойдет».

Отсюда мой вопрос: безопасно ли иногда полагаться на машинно-зависимое поведение, даже если стандарт C считает, что это вызывает UB, или «UB» действительно следует избегать, независимо от обстоятельств?


person Philip    schedule 23.05.2011    source источник
comment
Я склонен полагать, что могу предсказать результат. – Нет, вы можете вывести результат, используя определенный компилятор, используя определенные параметры, используя определенную версию среды выполнения на определенной платформе. Это была бы ужасная причина полагаться на UB.   -  person Ed S.    schedule 24.05.2011
comment
Вы думаете о поведении, определенном реализацией... у него есть определенное поведение, на которое вы можете положиться, но поведение отличается для каждой платформы, и в документации должно быть указано, что вы этого хотите. UB может быть непредсказуемым ... даже если вы думаете, что знаете, вы могли пропустить крайний случай, когда он не делает то, что вы ожидали.   -  person SoapBox    schedule 24.05.2011
comment
Обязательная ссылка xkcd: xkcd.com/292   -  person Hans Passant    schedule 24.05.2011
comment
Если вы хотите, чтобы этот фрагмент кода определялся реализацией вместо UB, просто измените 1 на 1U. Преобразование обратно в значение со знаком определяется реализацией и всегда (на современных машинах с дополнением до двух) будет тем, что вы хотите. Но зачем вообще писать такой неприятный код, когда есть чистые переносимые способы сделать это?   -  person R.. GitHub STOP HELPING ICE    schedule 24.05.2011
comment
@Hans: Хорошая мысль, совсем забыл об этом. :) +1   -  person user541686    schedule 24.05.2011
comment
@R, @SoapBox, @Ed и т. д.: Действительно ли поведение, определяемое реализацией, отличается от неопределенного поведения? Насколько я знаю, неопределенное поведение означает поведение, не определенное этим стандартом, что не означает, что поведение должно быть непредсказуемым; это просто означает, что это выходит за рамки стандарта. Таким образом, реализация может определять или не определять это. Это действительно отличается от реализации?   -  person user541686    schedule 24.05.2011
comment
@Mehrdad: Определенный термин «реализация» означает, что компилятор жалоб на стандарты может свободно выбирать из различных способов ведения дел, но должен документировать то, что он делает. Также возможно, что какой-то аспект поведения не определен; компилятор, оценивающий выражение a(b(),c()), может произвольно выбирать между двумя вариантами поведения: выполнить b() полностью, а затем c(), или выполнить c() целое число, а затем b(). Компилятор мог бы, если бы он был склонен, делать новый выбор каждый раз при выполнении кода. Обратите внимание, что нет неопределенного поведения, но последовательность неопределена.   -  person supercat    schedule 12.01.2013
comment
@supercat: Таким образом, компилятор должен документировать то, что он делает, но не может ли это быть чем-то совершенно случайным/неопределенным/и т. д.? (Скажем, в документации может быть сказано: Компилятор генерирует случайное число в соответствии с некоторым произвольным распределением и действует в соответствии с этим числом.) Поэтому, если вы не знаете, для какого компилятора пишете, на самом деле не имело бы значения, называл ли стандарт это реализацией определенной, а если бы он называл ее неуказанной или как-то еще, верно? В любом случае вы ничего не сможете предсказать... терминология никуда вас не приведет.   -  person user541686    schedule 12.01.2013
comment
@Mehrdad: в большинстве случаев стандарт накладывает определенные ограничения на то, что разрешено делать компилятору. Например, максимальное значение, которое может быть представлено в unsigned int, определяется реализацией. Если производитель компилятора хотел, чтобы его тип int мог принимать только значения до 8 675 309 и аварийно завершал работу при подаче большего значения, он мог сделать это при условии, что он задокументировал такое поведение и при условии, что его тип типа char не может обрабатывать значения большего размера.   -  person supercat    schedule 12.01.2013
comment
@supercat: Верно, но я хочу сказать, что независимо от сценария, это ваши единственные гарантии; другими словами, когда вы пытаетесь написать переносимый код, не имеет ни малейшего значения ни единого бита, являются ли гарантии, которые не даны, на самом деле определенными реализацией, неуказанными или неопределенными - суть в том, что бы это ни было, у вас есть только стандарт, на который можно положиться, поэтому на самом деле не имеет никакого значения, какова конкретная номенклатура/определение; в любом случае результат один и тот же: на него нельзя положиться.   -  person user541686    schedule 12.01.2013
comment
@Mehrdad: Если кто-то пишет переносимую программу на C, он должен воздерживаться от каких-либо действий, которые предполагают, что int может содержать значение больше 32767, если только вы не проверите значение INT_MAX. Однако если проверить значение INT_MAX или просто гарантировать, что int никогда не будет вызываться для хранения значения больше 32767, проблем не будет. Однако сценарии, включающие неопределенное поведение, не имеют никаких гарантий.   -  person supercat    schedule 12.01.2013
comment
@supercat: Конечно, я с тобой не согласен. Если есть определенные гарантии, то, конечно, есть определенные гарантии. :-) Я просто говорю, что тот факт, что это называется неопределенным поведением, ничего не значит - имеет значение только (отсутствие) гарантий, и все. . Беспокоиться (как и многие люди) о том, как это называется (рассматривать undefined и unspecified как два отдельных зверя, когда вы не смотрели на стандарт, чтобы понять, каковы ваши ограничения на самом деле), просто обращать внимание к неправильной вещи; в любом случае, у вас есть только гарантии в стандарте.   -  person user541686    schedule 12.01.2013
comment
@Mehrdad: термин «неопределенное поведение» означает, что компилятор может делать все что угодно. Напротив, неопределенный означает, что компилятор может произвольно выбирать из множества альтернатив. Код, который использует, должен быть написан для корректной работы с любой из возможных альтернатив; напротив, код не может быть написан так, чтобы правильно работать с Undefined Behavior (хотя возможно, что конкретная реализация может указывать поведение в случаях, которые стандарт оставляет неопределенными).   -  person supercat    schedule 12.01.2013


Ответы (7)


Нет, если вы также не оставляете свой компилятор прежним и документация вашего компилятора определяет неопределенное поведение.

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


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

Переполнение целого числа со знаком:

Если арифметика для типа int (например) переполняется, результат не определен. Одним из примеров является то, что INT_MAX + 1 не обязательно будет INT_MIN. Это поведение включает определенные классы оптимизации, которые важны для некоторого кода.

Например, зная, что INT_MAX + 1 не определено, можно оптимизировать X + 1 > X до true. Знание того, что умножение не может переполниться (потому что это было бы неопределенным), позволяет оптимизировать X * 2 / 2 до X. Хотя это может показаться тривиальным, такие вещи обычно обнаруживаются при встраивании и раскрытии макросов. Более важная оптимизация, которую это позволяет, предназначена для циклов <=, подобных этой:

for (i = 0; i <= N; ++i) { ... }

В этом цикле компилятор может предположить, что цикл повторится ровно N + 1 раз, если i не определено при переполнении, что позволяет использовать широкий диапазон оптимизаций цикла. , то компилятор должен предположить, что цикл, возможно, бесконечен (что происходит, если N равно INT_MAX), что затем отключает эти важные оптимизации цикла. Это особенно влияет на 64-битные платформы, так как очень много кода использует int в качестве переменных индукции.

person user541686    schedule 23.05.2011

No.

Компилятор использует неопределенное поведение при оптимизации кода. Известным примером является строгая семантика переполнения в компиляторе GCC (найдите strict-overflow здесь) Например, этот цикл

for (int i = 1; i != 0; ++i)
  ...

предположительно полагается на ваше «зависимое от машины» поведение переполнения целочисленного типа со знаком. Однако компилятор GCC в соответствии с правилами строгой семантики переполнения может (и будет) предположить, что увеличение переменной int может только сделать ее больше и никогда не уменьшить. Это предположение заставит GCC оптимизировать арифметику и вместо этого сгенерировать бесконечный цикл.

for (;;)
  ...

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

По сути, в языке C нет такого понятия, как «машинно-зависимое поведение». Все поведение определяется реализацией, а уровень реализации — это самый низкий уровень, до которого вы когда-либо могли добраться. Реализация изолирует вас от необработанной машины и изолирует вас идеально. Невозможно прорваться через эту изоляцию и добраться до фактической необработанной машины, если только реализация явно не разрешает вам это делать. Переполнение целого числа со знаком обычно не является одним из тех контекстов, в которых вам разрешен доступ к необработанной машине.

person AnT    schedule 23.05.2011
comment
Это предполагает, что тело цикла for не изменяет i. - person Konrad Borowski; 24.11.2013

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

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

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

person Rafe Kettler    schedule 23.05.2011
comment
вы знаете, как работает неопределенное поведение (и что это не изменится) — это было бы знанием непознаваемого. И даже если бы это было известно, вы, вероятно, ошиблись бы. - person Jim Balter; 24.05.2011
comment
Программа, как парень, поддерживающий ваш код, является массовым убийцей-психопатом, который знает, где вы живете... Делая это, я сделал бы себя тем парнем... - person mattnz; 24.05.2011

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

person Matteo Italia    schedule 23.05.2011

Если стандарт C (или другого языка) объявляет, что какой-то конкретный код будет иметь Undefined Behavior в какой-то ситуации, это означает, что компилятор C может генерировать код, который делает все, что он хочет в этой ситуации, оставаясь при этом совместимым с этим стандартом< /я>. Многие конкретные языковые реализации имеют задокументированное поведение, выходящее за рамки того, что требуется общим языковым стандартом. Например, компания Whizbang Compilers Inc. может явно указать, что ее конкретная реализация memcpy всегда будет копировать отдельные байты в порядке адресов. В таком компиляторе код вида:

  unsigned char z[256];
  z[0] = 0x53;
  z[1] = 0x4F;
  memcpy(z+2, z, 254);

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

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

person supercat    schedule 30.03.2012

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

И неопределенного поведения так ЛЕГКО избежать — не пишите такой код! Так почему же такие люди, как ты, хотят возиться с этим?

person Community    schedule 23.05.2011

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

person mattnz    schedule 24.05.2011