Why does printf("%f",0); give undefined behavior?
C++CPrintfImplicit ConversionUndefined BehaviorC++ Problem Overview
The statement
printf("%f\n",0.0f);
prints 0.
However, the statement
printf("%f\n",0);
prints random values.
I realize I'm exhibiting some kind of undefined behaviour, but I can't figure out why specifically.
A floating point value in which all the bits are 0 is still a valid float
with value of 0.
float
and int
are the same size on my machine (if that is even relevant).
Why does using an integer literal instead of a floating point literal in printf
cause this behavior?
P.S. the same behaviour can be seen if I use
int i = 0;
printf("%f\n", i);
C++ Solutions
Solution 1 - C++
The "%f"
format requires an argument of type double
. You're giving it an argument of type int
. That's why the behavior is undefined.
The standard does not guarantee that all-bits-zero is a valid representation of 0.0
(though it often is), or of any double
value, or that int
and double
are the same size (remember it's double
, not float
), or, even if they are the same size, that they're passed as arguments to a variadic function in the same way.
It might happen to "work" on your system. That's the worst possible symptom of undefined behavior, because it makes it difficult to diagnose the error.
N1570 7.21.6.1 paragraph 9:
> ... If any argument is not the correct type for the corresponding > conversion specification, the behavior is undefined.
Arguments of type float
are promoted to double
, which is why printf("%f\n",0.0f)
works. Arguments of integer types narrower than int
are promoted to int
or to unsigned int
. These promotion rules (specified by N1570 6.5.2.2 paragraph 6) do not help in the case of printf("%f\n", 0)
.
Note that if you pass a constant 0
to a non-variadic function that expects a double
argument, the behavior is well defined, assuming the function's prototype is visible. For example, sqrt(0)
(after #include <math.h>
) implicitly converts the argument 0
from int
to double
-- because the compiler can see from the declaration of sqrt
that it expects a double
argument. It has no such information for printf
. Variadic functions like printf
are special, and require more care in writing calls to them.
Solution 2 - C++
First off, as touched on in several other answers but not, to my mind, spelled out clearly enough: It does work to provide an integer in most contexts where a library function takes a double
or float
argument. The compiler will automatically insert a conversion. For instance, sqrt(0)
is well-defined and will behave exactly as sqrt((double)0)
, and the same is true for any other integer-type expression used there.
printf
is different. It's different because it takes a variable number of arguments. Its function prototype is
extern int printf(const char *fmt, ...);
Therefore, when you write
printf(message, 0);
the compiler does not have any information about what type printf
expects that second argument to be. It has only the type of the argument expression, which is int
, to go by. Therefore, unlike most library functions, it is on you, the programmer, to make sure the argument list matches the expectations of the format string.
(Modern compilers can look into a format string and tell you that you've got a type mismatch, but they're not going to start inserting conversions to accomplish what you meant, because better your code should break now, when you'll notice, than years later when rebuilt with a less helpful compiler.)
Now, the other half of the question was: Given that (int)0 and (float)0.0 are, on most modern systems, both represented as 32 bits all of which are zero, why doesn't it work anyway, by accident? The C standard just says "this isn't required to work, you're on your own", but let me spell out the two most common reasons why it wouldn't work; that will probably help you understand why it's not required.
First, for historical reasons, when you pass a float
through a variable argument list it gets promoted to double
, which, on most modern systems, is 64 bits wide. So printf("%f", 0)
passes only 32 zero bits to a callee expecting 64 of them.
The second, equally significant reason is that floating-point function arguments may be passed in a different place than integer arguments. For instance, most CPUs have separate register files for integers and floating-point values, so it might be a rule that arguments 0 through 4 go in registers r0 through r4 if they are integers, but f0 through f4 if they are floating-point. So printf("%f", 0)
looks in register f1 for that zero, but it's not there at all.
Solution 3 - C++
> Why does using an integer literal instead of a float literal cause this behavior?
Because printf()
doesn't have typed parameters besides the const char* formatstring
as the 1st one. It uses a c-style ellipsis (...
) for all the rest.
It's just decides how to interpret the values passed there according to the formatting types given in the format string.
You would have the same kind of undefined behavior as when trying
int i = 0;
const double* pf = (const double*)(&i);
printf("%f\n",*pf); // dereferencing the pointer is UB
Solution 4 - C++
Ordinarily when you call a function that expects a double
, but you provide an int
, the compiler will automatically convert to a double
for you. That doesn't happen with printf
, because the types of the arguments aren't specified in the function prototype - the compiler doesn't know that a conversion should be applied.
Solution 5 - C++
Using a mis-matched printf()
specifier "%f"
and type (int) 0
leads to undefined behavior.
> If a conversion specification is invalid, the behavior is undefined. C11dr §7.21.6.1 9
Candidate causes of UB.
-
It is UB per spec and the compile is ornery - 'nuf said.
-
double
andint
are of different sizes. -
double
andint
may pass their values using different stacks (general vs. FPU stack.) -
A
double 0.0
might not be defined by an all zero bit pattern. (rare)
Solution 6 - C++
This is one of those great opportunities to learn from your compiler warnings.
$ 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);
^
or
$ 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.
So, printf
is producing undefined behavior because you are passing it an incompatible type of argument.
Solution 7 - C++
I'm not sure what's confusing.
Your format string expects a double
; you provide instead an int
.
Whether the two types have the same bit width is utterly irrelevant, except that it may help you avoid getting hard memory violation exceptions from broken code like this.
Solution 8 - C++
"%f\n"
guarantees predictable result only when the second printf()
parameter has type of double
. Next, an extra arguments of variadic functions are subject of default argument promotion. Integer arguments fall under integer promotion, which never results in floating-point typed values. And float
parameters are promoted to double
.
To top it off: standard allows the second argument to be or float
or double
and nothing else.
Solution 9 - C++
Why it is formally UB has now been discussed in several answers.
The reason why you get specifically this behaviour is platform-dependent, but probably is the following:
printf
expects its arguments according to standard vararg propagation. That means afloat
will be adouble
and anything smaller than anint
will be anint
.- You are passing an
int
where the function expects adouble
. Yourint
is probably 32 bit, yourdouble
64 bit. That means that the four stack bytes starting at the place where the argument is supposed to sit are0
, but the following four bytes have arbitrary content. That's what is used for constructing the value which is displayed.
Solution 10 - C++
The main cause of this "undetermined value" issue stands in the cast of the pointer at the int
value passed to the printf
variable parameters section to a pointer at double
types that va_arg
macro carries out.
This causes a referencing to a memory area that was not completely initialized with value passed as parameter to the printf, because double
size memory buffer area is greater than int
size.
Therefore, when this pointer is dereferenced, it is returned an undetermined value, or better a "value" that contains in part the value passed as parameter to printf
, and for the remaining part could came from another stack buffer area or even a code area (raising a memory fault exception), a real buffer overflow.
It can consider these specific portions of semplificated code implementations of "printf" and "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..
....
> the real implementation in vprintf (considering gnu impl.) of double value parameters code case
> management is:
>
> if (__ldbl_is_dbl)
> {
> args_value[cnt].pa_double = va_arg (ap_save, double);
> ...
> }
>
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);
> references