Why wasn't a specifier for `float` defined in `printf`?

Solution 1:

Because in C variadic function calls, any float argument is promoted (i.e. converted) to a double, so printf is getting a double and would use va_arg(arglist, double) to get it inside its implementation.

In the past (C89 and K&R C) every float argument was converted to a double. The current standard omits this promotion for fixed arity functions which have an explicit prototype. It is related to (and details are explained in) the ABI & calling conventions of the implementation. Practically speaking, a float value would be often loaded into a double-floating point register when passed as an argument, but details can vary. Read the Linux x86-64 ABI specification as an example.

Also, there is no practical reason to give a specific format control string for float since you can adjust the width of the output (e.g. with %8.5f) as wanted, and %hd is much more useful (nearly necessary) in scanf than in printf

Beyond that, I guess that the reason (to omit %hf specifying float -promoted to double in caller- in printf) is historical: at first, C was a system programming language, not an HPC one (Fortran was preferred in HPC perhaps till the late 1990s) and float was not very important; it was (and still is) thought of like short, a way to lower memory consumption. And today's FPUs are fast enough (on desktop or server computers) to avoid using float except as a mean to use less memory. You basically should believe that every float is somewhere (perhaps inside the FPU or the CPU) converted to double.

Actually, your question might be paraphrased as : why %hd exists for printf (where it is basically useless, since printf is getting an int when you pass it some short; however scanf needs it!). I don't know why, but I imagine than in system programming it might be more useful.

You could spend time lobbying the next ISO C standard to get %hf accepted by printf for float (promoted to double at printf calls, like short-s get promoted to int), with undefined behavior when the double precision value is out of bound for float-s, and symetrically %hf accepted by scanf for float pointers. Good luck on that.

Solution 2:

Because of default argument promotions.

printf() is a variable argument function (... in its signature), all float arguments are promoted to double.

C11 §6.5.2.2 Function calls

6 If the expression that denotes the called function has a type that does not include a prototype, the integer promotions are performed on each argument, and arguments that have type float are promoted to double. These are called the default argument promotions.

7 The ellipsis notation in a function prototype declarator causes argument type conversion to stop after the last declared parameter. The default argument promotions are performed on trailing arguments.

Solution 3:

Due to the default argument promotions when calling variadic functions, float values are implicitly converted to double before the function call, and there is no way to pass a float value to printf. Since there's no way to pass a float value to printf, there's no need for an explicit format specifier for float values.

Having said that, AntoineL brought up an interesting point in a comment that %lf (currently used in scanf to correspond to an argument type double *) may have once stood for "long float", which was a type synonym in pre-C89 days, according to page 42 of the C99 rationale. By that logic, it might make sense that %f was intended to stand for a float value which has been converted to a double.


Regarding the hh and h length modifiers, %hhu and %hu present a well-defined use-case for these format specifiers: You can print the least significant byte of a large unsigned int or unsigned short without a cast, for example:

printf("%hhu\n", UINT_MAX); // This will print (unsigned char)  UINT_MAX
printf("%hu\n",  UINT_MAX); // This will print (unsigned short) UINT_MAX

It isn't particularly well defined what a narrowing conversion from int to char or short will result in, but it is at least implementation-defined, meaning the implementation is required to actually document this decision.

Following the pattern it should have been %hf.

Following the pattern you've observed, %hf should convert values outside of the range of float back to float. However, that kind of narrowing conversion from double to float results in undefined behaviour, and there's no such thing as an unsigned float. The pattern you see doesn't make sense.


To be formally correct, %lf does not denote a long double argument, and if you were to pass a long double argument you would be invoking undefined behaviour. It is explicit from the documentation that:

l (ell) ... has no effect on a following a, A, e, E, f, F, g, or G conversion specifier.

I'm surprised nobody else has picked up on this? %lf denotes a double argument, just like %f. If you want to print a long double, use %Lf (capital ell).

It should henceforth make sense that %lf for both printf and scanf correspond to double and double * arguments... %f is exceptional only because of the default argument promotions, for the reasons mentioned earlier.

... and %Ld does not mean long, either. What that means is undefined behaviour.

Solution 4:

From the ISO C11 standard, 6.5.2.2 Function calls /6 and /7, discussing function calls in the context of expressions (my emphasis):

6/ If the expression that denotes the called function has a type that does not include a prototype, the integer promotions are performed on each argument, and arguments that have type float are promoted to double. These are called the default argument promotions.

7/ If the expression that denotes the called function has a type that does include a prototype, the arguments are implicitly converted, as if by assignment, to the types of the corresponding parameters, taking the type of each parameter to be the unqualified version of its declared type. The ellipsis notation in a function prototype declarator causes argument type conversion to stop after the last declared parameter. The default argument promotions are performed on trailing arguments.

This means that any float arguments after the ... in the prototype are converted to double and the printf family of calls are defined that way (7.21.6.11 et seq):

int fprintf(FILE * restrict stream, const char * restrict format, ...);

So, since there's no way for printf()-family calls to actually receive a float, it makes little sense to have a special format specifier (or modifier) for it.

Solution 5:

Reading the C rationale, below fscanf, the following can be found:

A new feature of C99: The hh and ll length modifiers were added in C99. ll supports the new long long int type. hh adds the ability to treat character types the same as all other integer types; this can be useful in implementing macros such as SCNd8 in (see 7.18).

So supposedly the hh was added for the purpose of providing support for all of the new stdint.h types. This could explain why a length modifier was added for small integers but not for small floats.

It doesn't explain why C90 inconsistently had h but no hh though. The language as specified in C90 is not always consistent, simple as that. And later versions have inherited the inconsistency.