Почему 'd / = d' не генерирует исключение деления на ноль, когда d == 0?

81

Я не совсем понимаю, почему я не получаю исключение деления на ноль:

int d = 0;
d /= d;

Я ожидал получить деление на нулевое исключение, но вместо этого d == 1.

Почему не d /= dгенерирует исключение деления на ноль, когда d == 0?

Валерий Болдаков
источник
25
Это неопределенное поведение.
LF
51
Нет такой вещи, как исключение деления на ноль.
πάντα ῥεῖ
15
Чтобы прояснить некоторые комментарии: когда вы видите сообщение об «исключении деления на ноль», это означает, что операционная система сообщает вам, что что-то пошло не так. Это не исключение для C ++. В C ++ исключения генерируются throwоператором. Ничего другого (если только вы не находитесь в стране неопределенного поведения).
Пит Беккер,
9
В C ++ нет такой вещи, как « деление на нулевое исключение ».
Альгирдас Прейджиус
6
@ user11659763 "Вот почему это неопределенное поведение: оно полностью зависит от цели." - Это не то , что не определено означает поведение на всех ; то, что вы описываете, является поведением, определяемым реализацией . Неопределенное поведение - гораздо более сильное утверждение.
Марсельм

Ответы:

108

В C ++ нет исключения «Деление на ноль», которое можно было бы перехватить. Наблюдаемое вами поведение является результатом оптимизации компилятора:

  1. Компилятор предполагает, что неопределенного поведения не происходит
  2. Деление на ноль в C ++ - это неопределенное поведение
  3. Следовательно, предполагается , что код, который может вызвать деление на ноль, этого не делает.
    • Предполагается , что код, который должен вызывать деление на ноль, никогда не произойдет.
  4. Таким образом, компилятор делает вывод, что, поскольку Undefined Behavior не происходит, условия для Undefined Behavior в этом коде ( d == 0) не должны выполняться.
  5. Следовательно, d / dвсегда должно быть равно 1.

Тем не мение...

Мы можем заставить компилятор запускать «настоящее» деление на ноль с незначительной настройкой вашего кода.

volatile int d = 0;
d /= d; //What happens?

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

В основном это зависит от целевой среды. Это не вызовет программного исключения, но может (в зависимости от целевого ЦП) вызвать аппаратное исключение (целочисленное деление на ноль), которое не может быть обнаружено традиционным способом, так как может быть обнаружено программное исключение. Это определенно верно для ЦП x86 и большинства других (но не всех!) Архитектур.

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

Ксирема
источник
25
@Adrian И то, и другое в порядке, поскольку поведение не определено. Буквально все в порядке.
Jesper
9
«Деление на ноль в C ++ является неопределенным поведением» -> обратите внимание, что компилятор не может выполнить эту оптимизацию для типов с плавающей запятой в соответствии с IEE754. Он должен установить d равным NaN.
Вирсавия
6
@RichardHodges компилятору, работающему в соответствии с IEEE754, не разрешается производить оптимизацию для двойника: должно быть создано NaN.
Вирсавия
2
@formerlyknownas: Дело не в «оптимизации UB» - все равно «все может случиться»; просто продюсирование 1- это что угодно. Получение 14684554 должно происходить из-за того, что компилятор оптимизирует еще больше - он распространяет начальное d==0условие и поэтому может заключить не только «это либо 1, либо UB», но фактически «это UB, точка». Поэтому не нужно даже создавать код, загружающий константу 1.
hmakholm оставил Монику
1
Люди всегда предлагают volatile, чтобы предотвратить оптимизацию, но то, что составляет изменчивое чтение или запись, определяется реализацией.
Филипси
38

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

  • Компилятор может предположить это 0 / 0 == 1и соответствующим образом оптимизировать. По сути, это именно то, что он здесь сделал.
  • Компилятор также мог бы, если бы захотел, принять это 0 / 0 == 42и установить dэто значение.
  • Компилятор также может решить, что значение dявляется неопределенным, и, таким образом, оставить переменную неинициализированной, так что ее значение будет тем, что было ранее записано в выделенную для нее память. Некоторые из неожиданных значений, наблюдаемых в комментариях для других компиляторов, могут быть вызваны тем, что эти компиляторы делают что-то вроде этого.
  • Компилятор может также решить прервать программу или вызвать исключение всякий раз, когда происходит деление на ноль. Поскольку для этой программы компилятор может определить, что это всегда происходить, он может просто выдать код, чтобы вызвать исключение (или полностью прервать выполнение), и обработать остальную часть функции как недостижимый код.
  • Вместо того, чтобы вызывать исключение при делении на ноль, компилятор также может остановить программу и вместо этого запустить игру в пасьянс. Это также относится к "неопределенному поведению".
  • В принципе, компилятор мог даже выдать код, который заставил компьютер взорваться. всякий раз, когда происходит деление на ноль. В стандарте C ++ нет ничего, что запрещало бы это. (Для определенных типов приложений, таких как контроллер полета ракеты, это может даже считаться желательной функцией безопасности!)
  • Более того, стандарт явно допускает неопределенное поведение для «путешествия во времени» , так что компилятор может также сделать что-либо из вышеперечисленных (или что-то еще) до того, как произойдет деление на ноль. По сути, стандарт позволяет компилятору свободно изменять порядок операций до тех пор, пока наблюдаемое поведение программы не изменяется - но даже это последнее требование явно отменяется, если выполнение программы приведет к неопределенному поведению. Таким образом, фактически все поведение любой программы, которое в какой-то момент может вызвать неопределенное поведение, не определено!
  • Как следствие вышесказанного, компилятор может также просто предположить, что неопределенное поведение не происходит , поскольку одно допустимое поведение для программы, которая будет вести себя неопределенным образом на некоторых входных данных, заключается в том, что она просто ведет себя так, как если бы вход был чем-то еще . То есть, даже если исходное значение dне было известно во время компиляции, компилятор все равно мог бы предположить, что оно никогда не равно нулю, и соответствующим образом оптимизировать код. В конкретном случае кода OP это практически неотличимо от компилятора, просто предполагая это 0 / 0 == 1, но компилятор также может, например, предположить, что puts()in if (d == 0) puts("About to divide by zero!"); d /= d;никогда не выполняется!
Илмари Каронен
источник
29

Поведение целочисленного деления на ноль не определено стандартом C ++. Это не обязательно бросать исключение.

(Деление с плавающей запятой на ноль также не определено, но IEEE754 определяет это.)

Ваш компилятор оптимизируется d /= dпод, d = 1что является разумным выбором. Эту оптимизацию можно проводить, поскольку можно предположить, что в вашем коде нет неопределенного поведения - это dне может быть нулевым.

Вирсавия
источник
3
Важно быть предельно ясным, что также может произойти что-то еще, на такое поведение нельзя полагаться.
hyde
2
Когда вы говорите, что для компилятора разумно предположить, что "это dне может быть нулем", вы также предполагаете, что компилятор не видит строку: int d = 0;?? :)
Адриан Моул
6
Компилятор видит это, но, вероятно, не заботится. Дополнительная сложность кода, необходимая в безумно сложном компиляторе для такого пограничного случая, вероятно, того не стоит.
user4581301
1
@ user4581301 Взятие обоих вместе позволяет обнаружить зараженную ветвь, что позволяет сократить гораздо больше кода. Так что было бы полезно.
Дедупликатор
3
Итак, если вы написали «int d = 0; if (d == 0) printf (« d = zero \ n »); d / = d;», компилятор также может удалить printf.
gnasher729,
0

Обратите внимание, что в этом (и в других случаях) код может генерировать исключение C ++, используя числовые значения с повышенной безопасностью. https://github.com/boostorg/safe_numerics

Роберт Рэми
источник