Advertencia de C ++: división de doble por cero

98

Caso 1:

#include <iostream>

int main()
{
    double d = 15.50;
    std::cout<<(d/0.0)<<std::endl;
}

Se compila sin advertencias ni se imprime inf. Bien, C ++ puede manejar la división por cero ( véalo en vivo ).

Pero,

Caso 2:

#include <iostream>

int main()
{
    double d = 15.50;
    std::cout<<(d/0)<<std::endl;
}

El compilador da la siguiente advertencia ( véalo en vivo ):

warning: division by zero [-Wdiv-by-zero]
     std::cout<<(d/0)<<std::endl;

¿Por qué el compilador da una advertencia en el segundo caso?

Es 0 != 0.0?

Editar:

#include <iostream>

int main()
{
    if(0 == 0.0)
        std::cout<<"Same"<<std::endl;
    else
        std::cout<<"Not same"<<std::endl;
}

salida:

Same
Jayesh
fuente
9
Supongo que toma cero como un número entero en el segundo caso y deja caer una advertencia, incluso si el cálculo se haría usando el doble más adelante (que creo que debería ser el comportamiento cuando d es un doble).
Qubit
10
Es un problema de QoI, de verdad. Ni la advertencia ni la falta de advertencia es algo exigido por el propio estándar C ++. ¿Estás usando GCC?
StoryTeller - Unslander Monica
5
@StoryTeller ¿Qué es QoI? en.wikipedia.org/wiki/QoI ?
user202729
5
Con respecto a su última pregunta, "¿0 es lo mismo que 0,0?" La respuesta es que los valores son los mismos, pero como descubrió, eso no significa que sean idénticos. ¡Diferentes tipos! Al igual que 'A' no es idéntico a 65.
Mr Lister

Respuestas:

108

La división de coma flotante por cero está bien definida por IEEE y da infinito (ya sea positivo o negativo según el valor del numerador (o NaNpara ± 0) ).

Para los números enteros, no hay forma de representar el infinito y el lenguaje define la operación para que tenga un comportamiento indefinido, por lo que el compilador intenta ayudarlo a alejarse de esa ruta.

Sin embargo, en este caso, dado que el numerador es a double, el divisor ( 0) también debe promoverse a un doble y no hay razón para dar una advertencia aquí sin dar una advertencia, 0.0así que creo que esto es un error del compilador.

Motti
fuente
8
Sin embargo, ambas son divisiones de punto flotante. En d/0, 0se convierte al tipo de d.
43
Tenga en cuenta que no se requiere C ++ para usar IEEE 754 (aunque nunca vi un compilador usando un estándar diferente) ..
Yksisarvinen
1
@hvd, buen punto, si ese caso parece un error del compilador
Motti
14
Estoy de acuerdo en que debería advertir en ambos casos, o no advertir en ambos casos (dependiendo de cuál sea el manejo del compilador de la división flotante por cero)
MM
8
Estoy bastante seguro de que la división de coma flotante por cero también es UB, es solo que GCC lo implementa de acuerdo con IEEE 754. Sin embargo, no tienen que hacer eso.
Martin Bonner apoya a Monica el
42

En Standard C ++, ambos casos son comportamientos indefinidos . Puede suceder cualquier cosa, incluido el formateo de su disco duro. No debe esperar o confiar en "return inf. Ok" o cualquier otro comportamiento.

El compilador aparentemente decide dar una advertencia en un caso y no en el otro, pero esto no significa que un código esté bien y el otro no. Es solo una peculiaridad de la generación de advertencias del compilador.

Desde el estándar C ++ 17 [expr.mul] / 4:

El /operador binario produce el cociente y el %operador binario produce el resto de la división de la primera expresión por la segunda. Si el segundo operando de /o %es cero, el comportamiento no está definido.

MM
fuente
21
No es cierto, en la aritmética de coma flotante la división por cero está bien definida.
Motti
9
@Motti: si uno se limita al estándar C ++ solo, no existen tales garantías. El alcance de esta pregunta no está bien especificado, para ser franco.
StoryTeller - Unslander Monica
9
@StoryTeller Estoy bastante seguro (aunque no he mirado el documento estándar en sí para esto) que si lo std::numeric_limits<T>::is_iec559es true, entonces la división por cero Tno es UB (y en la mayoría de las plataformas es truepara doubley float, aunque para ser portátil, es necesario comprobarlo explícitamente con ifo if constexpr).
Daniel H
6
@DanielH - "Razonable" es bastante subjetivo. Si esta pregunta fuera etiquetada como abogado de lenguaje, habría un conjunto completamente diferente (mucho más pequeño) de suposiciones razonables.
StoryTeller - Unslander Monica
5
@MM Recuerde que es exactamente lo que dijo, pero nada más: indefinido no significa que no se imponen requisitos, significa que el estándar no impone requisitos . Yo diría que en este caso, el hecho de que una implementación se defina is_iec559como truesignifica que la implementación está documentando un comportamiento que el estándar deja sin definir. Es solo que este es un caso en el que la documentación de la implementación se puede leer mediante programación. Ni siquiera el único: lo mismo se aplica a los is_modulotipos enteros con signo.
12

Mi mejor suposición para responder a esta pregunta en particular sería que el compilador emite una advertencia antes de realizar la conversión de inta double.

Entonces, los pasos serían así:

  1. Parse expresión
  2. Operador aritmético /(T, T2) , donde T=double, T2=int.
  3. Compruebe que std::is_integral<T2>::valuees truey b == 0- esto activa una advertencia.
  4. Emitir advertencia
  5. Realizar conversión implícita de T2adouble
  6. Realice una división bien definida (ya que el compilador decidió usar IEEE 754).

Por supuesto, esto es especulación y se basa en especificaciones definidas por el compilador. Desde el punto de vista estándar, estamos tratando con posibles comportamientos indefinidos.


Tenga en cuenta que este es el comportamiento esperado de acuerdo con la documentación de GCC
(por cierto, parece que esta bandera no se puede usar explícitamente en GCC 8.1)

-Wdiv-by-zero
Advierte sobre la división de enteros en tiempo de compilación por cero. Esto es predeterminado. Para inhibir los mensajes de advertencia, use -Wno-div-by-zero. No se advierte sobre la división de coma flotante por cero, ya que puede ser una forma legítima de obtener infinitos y NaN.

Yksisarvinen
fuente
2
No es así como funcionan los compiladores de C ++. El compilador tiene que realizar una resolución de sobrecarga /para saber que es una división. Si el lado izquierdo hubiera sido un Fooobjeto, y hubiera un operator/(Foo, int), entonces podría ni siquiera ser una división. El compilador solo conoce su división cuando ha elegido built-in / (double, double)usar una conversión implícita del lado derecho. Pero eso significa que NO está haciendo una división por int(0), está haciendo una división por double(0).
MSalters
@MSalters Por favor, vea esto. Mi conocimiento sobre C ++ es limitado, pero de acuerdo con la referencia operator /(double, int)es ciertamente aceptable. Luego, dice que la conversión se realiza antes que cualquier otra acción, pero GCC podría hacer una verificación rápida si T2es de tipo entero b == 0y emitir una advertencia si es así. No estoy seguro de si es totalmente compatible con los estándares, pero los compiladores tienen total libertad para definir las advertencias y cuándo deben activarse.
Yksisarvinen
2
Estamos hablando del operador integrado aquí. Eso es gracioso. En realidad, no es una función, por lo que no puede tomar su dirección. Por lo tanto, no puede determinar si operator/(double,int)realmente existe. El compilador puede, por ejemplo, decidir optimizar a/bpara constante breemplazándolo con a * (1/b). Por supuesto, eso significa que ya no está llamando operator/(double,double)en tiempo de ejecución, sino más rápido operator*(double,double). Pero ahora es el optimizador el que se tropieza 1/0, la constante a la que tendría que alimentarseoperator*
MSalters
@MSalters Generalmente, la división de punto flotante no se puede reemplazar con la multiplicación, probablemente salvo en casos excepcionales, como 2.
user202729
2
@ user202729: GCC lo hace incluso para la división de enteros . Deje que eso se hunda por un momento. GCC reemplaza la división de enteros con la multiplicación de enteros. Sí, eso es posible, porque GCC sabe que está operando en un anillo (números módulo 2 ^ N)
MSalters
9

No entraré en la debacle UB / no UB en esta respuesta.

Solo quiero señalar eso 0y 0.0 somos diferentes a pesar de 0 == 0.0evaluarlos como verdaderos. 0es un intliteral y 0.0es un doubleliteral.

Sin embargo, en este caso, el resultado final es el mismo: d/0es una división de punto flotante porque des doble y, por 0lo tanto, se convierte implícitamente en doble.

bolov
fuente
5
No veo cómo esto es relevante, dado que las conversiones aritméticas habituales especifican que dividir a doublepor un intmedio al que intse convierte double , y se especifica en el estándar que 0convierte a 0.0 (conv.fpint / 2)
MM
@MM el OP quiere saber si 0es lo mismo que0.0
bolov
2
La pregunta dice "¿Es 0 != 0.0?". OP nunca pregunta si son "iguales". También me parece que la intención de la pregunta es si se d/0puede comportar de manera diferente ad/0.0
MM
2
@MM - El OP preguntó . Realmente no muestran una etiqueta de red SO decente con esas ediciones constantes.
StoryTeller - Unslander Monica
7

Yo diría que foo/0y nofoo/0.0 son lo mismo. Es decir, el efecto resultante de la primera (división de enteros o división de punto flotante) depende en gran medida del tipo de foo, mientras que no ocurre lo mismo con la segunda (siempre será una división de punto flotante).

Si alguno de los dos es UB es irrelevante. Citando el estándar:

El comportamiento indefinido admisible va desde ignorar la situación por completo con resultados impredecibles, hasta comportarse durante la traducción o ejecución del programa de manera documentada característica del entorno (con o sin la emisión de un mensaje de diagnóstico) , hasta terminar una traducción o ejecución (con la emisión de un mensaje de diagnóstico).

(El énfasis es mío)

Considere la advertencia " Sugerir paréntesis alrededor de la asignación usada como valor de verdad ": la forma de decirle al compilador que realmente desea usar el resultado de una asignación es ser explícito y agregar paréntesis alrededor de la asignación. La declaración resultante tiene el mismo efecto, pero le dice al compilador que sabe lo que está haciendo. Se puede decir lo mismo sobre foo/0.0: Ya que le está diciendo explícitamente al compilador "Esta es una división de punto flotante" usando en 0.0lugar de 0, el compilador confía en usted y no emitirá una advertencia.

Cássio Renan
fuente
1
Ambos deben someterse a las conversiones aritméticas habituales para obtener un tipo común, lo que dejará en ambos casos la división de punto flotante.
Shafik Yaghmour
@ShafikYaghmour No entendiste el punto en la respuesta. Tenga en cuenta que nunca mencioné cuál es el tipo de foo. Eso es intencional. Su afirmación solo es cierta en el caso de que foosea ​​un tipo de punto flotante.
Cássio Renan
No lo hice, el compilador tiene información de tipo y entiende las conversiones, tal vez un analizador estático basado en texto puede ser detectado por tales cosas, pero el compilador no debería hacerlo.
Shafik Yaghmour
Mi punto es que sí, el compilador conoce las conversiones aritméticas habituales, pero elige no emitir una advertencia cuando el programador está siendo explícito. El punto es que probablemente esto no sea un error, sino un comportamiento intencional.
Cássio Renan
Entonces, la documentación que señalé es incorrecta, ya que ambos casos son división de punto flotante. Entonces, la documentación es incorrecta o el diagnóstico tiene un error.
Shafik Yaghmour
4

Esto parece un error de gcc, la documentación -Wno-div-by-zero dice claramente :

No advierta sobre la división de enteros en tiempo de compilación por cero. No se advierte sobre la división en coma flotante por cero, ya que puede ser una forma legítima de obtener infinitos y NaN.

y después de las conversiones aritméticas habituales cubiertas en [expr.arith.conv] ambos operandos serán dobles :

Muchos operadores binarios que esperan operandos de tipo aritmético o enumeración provocan conversiones y producen tipos de resultados de forma similar. El propósito es producir un tipo común, que también es el tipo de resultado. Este patrón se denomina conversiones aritméticas habituales, que se definen de la siguiente manera:

...

- De lo contrario, si alguno de los operandos es doble, el otro se convertirá en doble.

y [expr.mul] :

Los operandos de * y / deberán tener un tipo de enumeración aritmético o sin ámbito; los operandos de% deberán tener un tipo de enumeración integral o sin ámbito. Las conversiones aritméticas habituales se realizan en los operandos y determinan el tipo de resultado.

Con respecto a si el punto flotante dividir por cero es un comportamiento indefinido y cómo lo maneja la implementación diferente, parece mi respuesta aquí . TL; DR; Parece que gcc se ajusta al Anexo F wrt para dividir el punto flotante por cero, por lo que undefined no juega un papel aquí. La respuesta sería diferente para clang.

Shafik Yaghmour
fuente
2

La división de coma flotante por cero se comporta de manera diferente a la división entera por cero.

El estándar de coma flotante IEEE diferencia entre + inf y -inf, mientras que los enteros no pueden almacenar infinito. La división de enteros por resultado cero es un comportamiento indefinido. La división de coma flotante por cero está definida por el estándar de coma flotante y da como resultado + inf o -inf.

Rizwan
fuente
2
Esto es cierto, pero no está claro cómo es relevante para la pregunta, ya que la división de punto flotante se realiza en ambos casos . No hay división de enteros en el código de OP.
Konrad Rudolph