¿Por qué 'd / = d' no arroja una excepción de división por cero cuando d == 0?

81

No entiendo muy bien por qué no obtengo una excepción de división por cero:

int d = 0;
d /= d;

Esperaba obtener una división por excepción cero, pero en cambio d == 1.

¿Por qué no d /= dlanza una excepción de división por cero cuando d == 0?

Valerii Boldakov
fuente
25
Ese es un comportamiento indefinido.
LF
51
No existe la excepción de división por cero.
πάντα ῥεῖ
15
Para aclarar algunos de los comentarios: cuando ve un mensaje sobre una "excepción de división por cero", es el sistema operativo que le dice que algo salió mal. Es no una excepción de C ++. En C ++, las excepciones se generan mediante una throwdeclaración. Nada más (a menos que estés en una tierra de comportamiento indefinido).
Pete Becker
9
No existe la " excepción de división por cero " en C ++.
Algirdas Preidžius
6
@ user11659763 "Por eso es un comportamiento indefinido: depende completamente del objetivo". - Eso no es lo que significa comportamiento indefinido en absoluto ; lo que está describiendo es el comportamiento definido por la implementación . El comportamiento indefinido es una afirmación mucho más fuerte.
marcelm

Respuestas:

108

C ++ no tiene una excepción de "División por cero" para detectar. El comportamiento que está observando es el resultado de las optimizaciones del compilador:

  1. El compilador asume que el comportamiento indefinido no ocurre
  2. La división por cero en C ++ es un comportamiento indefinido
  3. Por lo tanto, se presume que el código que puede causar una división por cero no lo hace.
    • Y se presume que el código que debe causar una división por cero nunca sucederá
  4. Por lo tanto, el compilador deduce que debido a que el comportamiento indefinido no ocurre, las condiciones para el comportamiento indefinido en este código ( d == 0) no deben ocurrir.
  5. Por lo tanto, d / dsiempre debe ser igual a 1.

Sin embargo...

Podemos forzar al compilador a activar una división "real" por cero con un pequeño ajuste a su código.

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

Así que ahora la pregunta sigue siendo: ahora que básicamente hemos forzado al compilador a permitir que esto suceda, ¿qué sucede? Es un comportamiento indefinido, pero ahora hemos evitado que el compilador optimice este comportamiento indefinido.

Principalmente, depende del entorno de destino. Esto no activará una excepción de software, pero puede (dependiendo de la CPU de destino) activar una excepción de hardware (un entero-dividir por cero), que no se puede detectar de la manera tradicional. Este es definitivamente el caso de una CPU x86 y la mayoría de las otras arquitecturas (¡pero no todas!).

Sin embargo, existen métodos para lidiar con la excepción de hardware (si ocurre) en lugar de simplemente dejar que el programa se bloquee: mire esta publicación para ver algunos métodos que podrían ser aplicables: Captura de excepción: dividir por cero . Tenga en cuenta que varían de un compilador a otro.

Xirema
fuente
25
@Adrian Ambos están perfectamente bien ya que el comportamiento no está definido. Literalmente todo está bien.
Jesper Juhl
9
"La división por cero en C ++ es un comportamiento indefinido" -> tenga en cuenta que el compilador no puede realizar esta optimización para tipos de punto flotante según IEE754. Debe establecer d en NaN.
Betsabé
6
@RichardHodges un compilador que opera bajo IEEE754 no puede realizar la optimización para un doble: se debe producir NaN.
Betsabé
2
@formerlyknownas: No se trata de "optimizar la UB"; sigue siendo cierto que "todo puede pasar"; es solo que producir 1es algo perfectamente válido. Obtener 14684554 debe ser porque el compilador optimiza aún más - propaga la d==0condición inicial y por lo tanto puede concluir no solo "esto es 1 o UB" sino que de hecho "esto es UB, punto". Por lo tanto, ni siquiera se molesta en producir código que cargue la constante 1.
hmakholm dejó a Monica el
1
La gente siempre sugiere volátiles para evitar la optimización, pero lo que constituye una lectura o escritura volátil está definido por la implementación.
philipxy
38

Solo para complementar las otras respuestas, el hecho de que la división por cero sea un comportamiento indefinido significa que el compilador es libre de hacer cualquier cosa en los casos en que suceda:

  • El compilador puede asumir eso 0 / 0 == 1y optimizar en consecuencia. Eso es efectivamente lo que parece haber hecho aquí.
  • El compilador también podría, si quisiera, asumir eso 0 / 0 == 42y establecer dese valor.
  • El compilador también podría decidir que el valor de des indeterminado y, por lo tanto, dejar la variable sin inicializar, de modo que su valor sea lo que haya sido escrito previamente en la memoria asignada. Algunos de los valores inesperados observados en otros compiladores en los comentarios pueden ser causados ​​por esos compiladores que hacen algo como esto.
  • El compilador también puede decidir abortar el programa o generar una excepción siempre que se produzca una división por cero. Dado que, para este programa, el compilador puede determinar que esto siempre sucederá, simplemente puede emitir el código para generar la excepción (o abortar la ejecución por completo) y tratar el resto de la función como código inalcanzable.
  • En lugar de generar una excepción cuando se produce la división por cero, el compilador también podría optar por detener el programa e iniciar un juego de solitario. Eso también cae bajo el paraguas de "comportamiento indefinido".
  • En principio, el compilador podría incluso emitir un código que provocara que la computadora explotara cada vez que se produjera una división por cero. No hay nada en el estándar C ++ que prohíba esto. (Para ciertos tipos de aplicaciones, como un controlador de vuelo de misiles, ¡esto incluso podría considerarse una característica de seguridad deseable!)
  • Además, el estándar permite explícitamente un comportamiento indefinido para "viajar en el tiempo" , de modo que el compilador también puede hacer cualquiera de las cosas anteriores (o cualquier otra cosa) antes de que ocurra la división por cero. Básicamente, el estándar permite al compilador reordenar libremente las operaciones siempre que no se cambie el comportamiento observable del programa, pero incluso ese último requisito se renuncia explícitamente si la ejecución del programa da como resultado un comportamiento indefinido. Entonces, en efecto, el comportamiento completo de cualquier ejecución de programa que, en algún momento, desencadenaría un comportamiento indefinido, ¡no está definido!
  • Como consecuencia de lo anterior, el compilador también puede simplemente asumir que el comportamiento indefinido no ocurre , ya que un comportamiento permisible para un programa que se comportaría de manera indefinida en algunas entradas es que simplemente se comporte como si la entrada hubiera sido algo. otra cosa . Es decir, incluso si dno se conocía el valor original de en el momento de la compilación, el compilador aún podría asumir que nunca es cero y optimizar el código en consecuencia. En el caso particular del código de OP, esto es efectivamente indistinguible del compilador simplemente asumiendo eso 0 / 0 == 1, pero el compilador también podría, por ejemplo, asumir que el puts()in if (d == 0) puts("About to divide by zero!"); d /= d;nunca se ejecuta.
Ilmari Karonen
fuente
29

El comportamiento de la división de enteros por cero no está definido por el estándar C ++. Se no se requiere para producir una excepción.

(La división de coma flotante por cero también está indefinida, pero IEEE754 la define).

Su compilador se está optimizando d /= d, efectivamente, lo d = 1cual es una elección razonable. Se le permite realizar esta optimización ya que se le permite asumir que no hay un comportamiento indefinido en su código, que dno puede ser cero.

Betsabé
fuente
3
Es importante ser más claro, que también podría suceder algo más, IOW no se puede confiar en este comportamiento.
hyde
2
Cuando dice que es razonable que el compilador asuma que "eso dno puede ser cero", ¿también asume que el compilador no ve la línea: int d = 0;?? :)
Adrian Mole
6
El compilador lo ve, pero probablemente no le importe. La complejidad de código adicional requerida en el ya loco compilador complejo para un caso de borde como este probablemente no valga la pena.
user4581301
1
@ user4581301 Tomar ambos juntos le permite detectar una rama envenenada, lo que le permite podar mucho más código. Entonces sería útil.
Deduplicador
3
Entonces, si escribió "int d = 0; if (d == 0) printf (" d = zero \ n "); d / = d;" el compilador también puede eliminar el printf.
gnasher729