Почему "d/= d" не выбрасывает исключение деления на ноль, когда d == 0?
Я не совсем понимаю, почему я не получаю деление на ноль исключений:
int d = 0;
d /= d;
Я ожидал получить деление на ноль исключений, но вместо этого d == 1
.
Почему d /= d
не создает исключение деления на ноль, когда d == 0
?
Ответы
Ответ 1
C++ не имеет исключения "деление на ноль". Поведение, которое вы наблюдаете, является результатом оптимизации компилятора:
- Компилятор предполагает, что Undefined Behavior не происходит
- Деление на ноль в C++ - неопределенное поведение
- Поэтому предполагается, что код, который может вызвать деление на ноль, этого не делает.
- И код, который должен вызывать деление на ноль, никогда не случается
- Следовательно, компилятор делает вывод, что поскольку неопределенного поведения не происходит, то условия для неопределенного поведения в этом коде (
d == 0
) не должны выполняться
- Поэтому
d / d
всегда должен быть равен 1.
Однако...
Мы можем заставить компилятор вызвать "реальное" деление на ноль с небольшим изменением кода.
volatile int d = 0;
d /= d; //What happens?
Итак, теперь остается вопрос: теперь, когда мы в основном заставили компилятор позволить этому случиться, что происходит? Это неопределенное поведение, но теперь мы не позволяем компилятору оптимизировать это неопределенное поведение.
Главным образом, это зависит от целевой среды. Это не вызовет программную исключительную ситуацию, но может (в зависимости от целевого ЦП) вызвать аппаратную исключительную ситуацию (целочисленное деление на ноль), которая не может быть перехвачена традиционным способом, которым может быть обработана программная исключительная ситуация. Это определенно относится к процессору x86 и большинству других (но не всех!) Архитектур.
Однако существуют способы борьбы с аппаратным исключением (если оно происходит), а не просто сбоем программы: посмотрите в этом посте некоторые методы, которые могут быть применимы: Перехват исключения: деление на ноль. Обратите внимание, что они варьируются от компилятора к компилятору.
Ответ 2
В дополнение к другим ответам тот факт, что деление на ноль является неопределенным поведением, означает, что компилятор может делать все, что угодно:
- Компилятор может предположить, что
0 / 0 == 1
и оптимизировать соответственно. Это эффективно то, что, похоже, здесь и сделано.
- При желании компилятор может предположить, что
0 / 0 == 42
и установить d
на это значение.
- Компилятор также может решить, что значение
d
является неопределенным, и, таким образом, оставить переменную неинициализированной, так что ее значение будет таким, каким оно было ранее записано в выделенную для него память. Некоторые неожиданные значения, наблюдаемые в других компиляторах в комментариях, могут быть вызваны тем, что эти компиляторы делают что-то подобное.
- Компилятор также может решить прервать программу или вызвать исключение всякий раз, когда происходит деление на ноль. Поскольку для этой программы компилятор может определить, что это всегда будет происходить, он может просто выдать код, чтобы вызвать исключение (или полностью прервать выполнение) и обработать остальную часть функции как недоступный код.
- Вместо того, чтобы вызывать исключение, когда происходит деление на ноль, компилятор также может выбрать остановку программы и начать игру в пасьянс. Это также подпадает под "неопределенное поведение".
- В принципе, компилятор может даже выдавать код, который заставляет компьютер взрываться всякий раз, когда происходит деление на ноль. В стандарте C++ нет ничего, что могло бы запретить это. (Для некоторых видов применения, например, для управления полетом ракеты, это может даже считаться желательной функцией безопасности!)
- Кроме того, стандарт явно разрешает неопределенному поведению "путешествовать во времени", так что компилятор может также выполнить любую из указанных выше операций (или что-либо еще) до того, как произойдет деление на ноль. По сути, стандарт позволяет компилятору свободно переупорядочивать операции до тех пор, пока наблюдаемое поведение программы не изменится, но даже это последнее требование явно отменено, если выполнение программы приведет к неопределенному поведению. Таким образом, по сути, все поведение любого выполнения программы, которое в какой-то момент вызовет неопределенное поведение, не определено!
- Как следствие вышесказанного, компилятор может также просто предположить, что неопределенное поведение не происходит, поскольку одно допустимое поведение для программы, которая будет вести себя неопределенным образом на некоторых входных данных, - это просто вести себя так, как будто входные данные были чем-то остальное. То есть, даже если исходное значение
d
не было известно во время компиляции, компилятор все равно мог предположить, что оно никогда не обнулялось, и соответствующим образом оптимизировать код. В частном случае кода OP это фактически неотличимо от компилятора, только если предположить, что 0 / 0 == 1
, но компилятор также может, например, предположить, что puts()
в if (d == 0) puts("About to divide by zero!"); d /= d;
никогда не будет выполнен!
Ответ 3
Поведение целочисленного деления на ноль не определено стандартом C++. not не требуется для создания исключения.
(Деление с плавающей точкой на ноль также не определено, но IEEE754 определяет его.)
Ваш компилятор оптимизирует d /= d
для эффективного d = 1
, что является разумным выбором. Это позволило выполнить эту оптимизацию, поскольку позволило предположить, что в вашем коде нет неопределенного поведения - то есть d
не может быть нулем.
Ответ 4
Обратите внимание, что в этом (и других случаях) ваш код может генерировать исключение C++, используя числовые значения повышения безопасности. https://github.com/boostorg/safe_numerics