La declaración
printf("%f\n",0.0f);
imprime 0.
Sin embargo, la declaración
printf("%f\n",0);
imprime valores aleatorios.
Me doy cuenta de que estoy exhibiendo algún tipo de comportamiento indefinido, pero no puedo entender por qué específicamente.
Un valor de punto flotante en el que todos los bits son 0 sigue siendo válido floatcon un valor de 0
floaty intson del mismo tamaño en mi máquina (si eso es incluso relevante).
¿Por qué el uso de un literal entero en lugar de un literal de punto flotante printfcausa este comportamiento?
PD: se puede ver el mismo comportamiento si uso
int i = 0;
printf("%f\n", i);
c++
c
printf
implicit-conversion
undefined-behavior
Trevor Hickey
fuente
fuente

printfestá esperando undouble, y le está dando unint.floatyintpuede ser del mismo tamaño en su máquina, pero en0.0frealidad se convierte en adoublecuando se inserta en una lista de argumentos variadic (yprintfespera eso). En resumen, no está cumpliendo con su parte del trato enprintffunción de los especificadores que utiliza y los argumentos que proporciona.(uint64_t)0lugar de0y ver si todavía obtiene un comportamiento aleatorio (asumiendodoubleyuint64_ttiene el mismo tamaño y alineación). Es probable que la salida siga siendo aleatoria en algunas plataformas (por ejemplo, x86_64) debido a que se pasan diferentes tipos en diferentes registros.Respuestas:
El
"%f"formato requiere un argumento de tipodouble. Le está dando un argumento de tipoint. Por eso el comportamiento no está definido.La norma no garantiza que todos los bits de cero es una representación válida de
0.0(aunque a menudo es), o de cualquierdoublevalor, o queintydoubleson del mismo tamaño (recuerda que esdouble, nofloat), o, incluso si son de la misma size, que se pasan como argumentos a una función variable de la misma manera.Puede suceder que "funcione" en su sistema. Ese es el peor síntoma posible de comportamiento indefinido, porque dificulta el diagnóstico del error.
N1570 7.21.6.1 párrafo 9:
floatSe promueven argumentos de tipodouble, razón por la cualprintf("%f\n",0.0f)funciona. Argumentos de tipos enteros más estrechos que los queintse promuevenintao aunsigned int. Estas reglas de promoción (especificadas por N1570 6.5.2.2 párrafo 6) no ayudan en el caso deprintf("%f\n", 0).Tenga en cuenta que si pasa una constante
0a una función no variada que espera undoubleargumento, el comportamiento está bien definido, asumiendo que el prototipo de la función es visible. Por ejemplo,sqrt(0)(after#include <math.h>) convierte implícitamente el argumento0deintadouble, porque el compilador puede ver en la declaraciónsqrtque espera undoubleargumento. No tiene tal información paraprintf. Las funciones variables comoprintfson especiales y requieren más cuidado al escribirles las llamadas.fuente
double, nofloatpor lo que espera supuesto el ancho de la OP no puede (probablemente no lo hace). En segundo lugar, la suposición de que el cero entero y el cero en coma flotante tienen el mismo patrón de bits tampoco se cumple. Buen trabajofloatse asciende adoublesin explicar por qué, pero ese no era el punto principal.printf, aunque gcc, por ejemplo, tiene algunos para que pueda diagnosticar errores ( si la cadena de formato es literal). El compilador puede ver la declaración deprintffrom<stdio.h>, que le dice que el primer parámetro es aconst char*y el resto se indica con, .... No,%fes paradouble(yfloatse asciende adouble) y%lfes paralong double. El estándar C no dice nada sobre una pila. Especifica el comportamiento deprintfsolo cuando se llama correctamente.floatpasado aprintfse promueve adouble; no hay nada mágico en eso, es solo una regla de lenguaje para llamar a funciones variadas.printfél mismo sabe, a través de la cadena de formato, lo que la persona que llama afirmó pasarle; si esa afirmación es incorrecta, el comportamiento no está definido.lmodificador de longitud "no tiene efecto en un siguientea,A,e,E,f,F,g, oGespecificador de conversión", el modificador de longitud para unalong doubleconversión esL. (@ robertbristow-johnson también podría estar interesado)En primer lugar, como tocado en varias otras respuestas, pero no, en mi opinión, explicado con suficiente claridad: Se hace el trabajo para proporcionar un número entero en la mayoría de los contextos en los que una función de biblioteca toma una
doubleofloatargumento. El compilador insertará automáticamente una conversión. Por ejemplo,sqrt(0)está bien definido y se comportará exactamente comosqrt((double)0), y lo mismo ocurre con cualquier otra expresión de tipo entero que se use allí.printfes diferente. Es diferente porque requiere un número variable de argumentos. Su función prototipo esextern int printf(const char *fmt, ...);Por tanto, cuando escribes
printf(message, 0);el compilador no tiene ninguna información sobre qué tipo
printfespera que sea ese segundo argumento. Solo tiene el tipo de expresión de argumento, que esint, para pasar. Por lo tanto, a diferencia de la mayoría de las funciones de la biblioteca, depende de usted, el programador, asegurarse de que la lista de argumentos coincida con las expectativas de la cadena de formato.(Los compiladores modernos pueden buscar en una cadena de formato y decirle que tiene una falta de coincidencia de tipos, pero no van a comenzar a insertar conversiones para lograr lo que quería decir, porque mejor su código debería romperse ahora, cuando lo notará , que años después cuando se reconstruyó con un compilador menos útil).
Ahora, la otra mitad de la pregunta era: dado que (int) 0 y (float) 0.0 están, en la mayoría de los sistemas modernos, ambos representados como 32 bits, todos los cuales son cero, ¿por qué no funciona de todos modos, por accidente? El estándar C simplemente dice "esto no es necesario para funcionar, estás solo", pero déjame explicarte las dos razones más comunes por las que no funcionaría; eso probablemente le ayudará a comprender por qué no es necesario.
Primero, por razones históricas, cuando se pasa a
floattravés de una lista de argumentos variables, se asciende a ladoubleque, en la mayoría de los sistemas modernos, tiene 64 bits de ancho. Por lo tanto,printf("%f", 0)pasa solo 32 bits cero a un destinatario que espera 64 de ellos.La segunda razón, igualmente significativa, es que los argumentos de la función de punto flotante pueden pasarse en un lugar diferente al de los argumentos enteros. Por ejemplo, la mayoría de las CPU tienen archivos de registro separados para enteros y valores de punto flotante, por lo que podría ser una regla que los argumentos del 0 al 4 vayan en los registros r0 a r4 si son enteros, pero de f0 a f4 si son de punto flotante. Entonces
printf("%f", 0)busca en el registro f1 ese cero, pero no está allí en absoluto.fuente
().AL. (Sí, esto significa que la implementación deva_arges mucho más complicada de lo que solía ser.)ret ninstrucción del 8086, dondenera un número entero codificado, que por lo tanto no era aplicable para funciones variadas. Sin embargo, no sé si algún compilador de C realmente lo aprovechó (los compiladores que no son de C ciertamente lo hicieron).Por lo general, cuando llama a una función que espera un
double, pero proporciona unint, el compilador se convertirá automáticamente en adoublepor usted. Eso no sucede conprintf, porque los tipos de argumentos no se especifican en el prototipo de la función; el compilador no sabe que se debe aplicar una conversión.fuente
printf()en particular, está diseñado para que sus argumentos puedan ser de cualquier tipo. Debe saber qué tipo espera cada elemento en la cadena de formato y debe proporcionarlo correctamente.Porque
printf()no tiene parámetros escritos ademásconst char* formatstringdel primero. Utiliza una elipsis de estilo c (...) para el resto.Simplemente decide cómo interpretar los valores pasados allí de acuerdo con los tipos de formato dados en la cadena de formato.
Tendrías el mismo tipo de comportamiento indefinido que cuando intentas
int i = 0; const double* pf = (const double*)(&i); printf("%f\n",*pf); // dereferencing the pointer is UBfuente
printfpodrían funcionar de esa manera (excepto que los elementos pasados son valores, no direcciones). El estándar C no especifica cómo funcionanprintfy otras funciones variadas, solo especifica su comportamiento. En particular, no se mencionan los marcos de pila.printftiene un parámetro escrito, la cadena de formato, que es de tipoconst char*. Por cierto, la pregunta está etiquetada como C y C ++, y C es realmente más relevante; Probablemente no lo hubiera usadoreinterpret_castcomo ejemplo.El uso de un
printf()especificador"%f"y un tipo(int) 0que no coinciden conduce a un comportamiento indefinido.Causas candidatas de UB.
Es UB por especificación y la compilación es insoportable '', dijo Nuf.
doubleyintson de diferentes tamaños.doubleyintpueden pasar sus valores usando diferentes pilas (general frente a pila FPU ).Es
double 0.0posible que A no esté definido por un patrón de bits completamente cero. (raro)fuente
Esta es una de esas grandes oportunidades para aprender de las advertencias de su compilador.
$ gcc -Wall -Wextra -pedantic fnord.c fnord.c: In function ‘main’: fnord.c:8:2: warning: format ‘%f’ expects argument of type ‘double’, but argument 2 has type ‘int’ [-Wformat=] printf("%f\n",0); ^o
$ clang -Weverything -pedantic fnord.c fnord.c:8:16: warning: format specifies type 'double' but the argument has type 'int' [-Wformat] printf("%f\n",0); ~~ ^ %d 1 warning generated.Entonces,
printfestá produciendo un comportamiento indefinido porque le está pasando un tipo de argumento incompatible.fuente
No estoy seguro de qué es confuso.
Su cadena de formato espera un
double; en su lugar, proporciona unint.Si los dos tipos tienen el mismo ancho de bits es completamente irrelevante, excepto que puede ayudarlo a evitar las excepciones de violación de la memoria por un código roto como este.
fuente
intsería aceptable aquí.intcalificaría como un patrón flotante válido? El complemento a dos y las diversas codificaciones de punto flotante no tienen casi nada en común.0a un argumento escritodoublehará lo correcto. No es obvio para un principiante que el compilador no haga la misma conversión para losprintfespacios de argumentos abordados por%[efg]."%f\n"garantiza un resultado predecible solo cuando el segundoprintf()parámetro tiene el tipo dedouble. A continuación, los argumentos adicionales de las funciones variadas están sujetos a la promoción de argumentos predeterminados. Los argumentos enteros se incluyen en la promoción de enteros, lo que nunca da como resultado valores con tipo de punto flotante. Y losfloatparámetros se promueven adouble.Para colmo: estándar permite que el segundo argumento sea o
floatodoubley nada más.fuente
Por qué es formalmente UB ahora se ha discutido en varias respuestas.
La razón por la que obtiene específicamente este comportamiento depende de la plataforma, pero probablemente sea la siguiente:
printfespera sus argumentos de acuerdo con la propagación estándar de vararg. Eso significa que unfloatserá undoubley cualquier cosa más pequeña que unintserá unint.intdonde la función espera undouble. Suintes probablemente de 32 bits, eldouble64 bits. Eso significa que los cuatro bytes de la pila que comienzan en el lugar donde se supone que debe estar el argumento están0, pero los siguientes cuatro bytes tienen contenido arbitrario. Eso es lo que se utiliza para construir el valor que se muestra.fuente
La causa principal de este problema de "valor indeterminado" se encuentra en la conversión del puntero en el
intvalor pasado a laprintfsección de parámetros de variable a un puntero en losdoubletipos queva_argrealiza la macro.Esto provoca una referencia a un área de memoria que no se inicializó completamente con el valor pasado como parámetro a printf, porque el
doubletamaño del área de la memoria intermedia de memoria es mayor que elinttamaño.Por lo tanto, cuando este puntero es desreferenciado, se devuelve un valor indeterminado, o mejor un "valor" que contiene en parte el valor pasado como parámetro
printf, y la parte restante podría provenir de otra área de búfer de pila o incluso un área de código ( levantando una excepción de falla de memoria), un desbordamiento real del búfer .Puede considerar estas porciones específicas de implementaciones de código semplificado de "printf" y "va_arg" ...
printf
va_list arg; .... case('%f') va_arg ( arg, double ); //va_arg is a macro, and so you can pass it the "type" that will be used for casting the int pointer argument of printf.. ....va_arg
char *p = (double *) &arg + sizeof arg; //printf parameters area pointer double i2 = *((double *)p); //casting to double because va_arg(arg, double) p += sizeof (double);fuente