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

It looks like it could have been, there are (at least in C99) length modifiers that can be applied to int: %hhd, %hd, %ld and %lld mean signed char, short, long and long long. There is even a length modifier applicable to double: %Lf means long double.

The question is why did they omit float? Following the pattern, it might have been %hf.



Solution 1:[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:[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:[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:[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:[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.

Solution 6:[6]

When C was invented, all floating-point values were converted to a common type (i.e. double) before being used in computations or passed to functions (including) printf, so there was no need for printf to make any distinctions among floating-point types.

In the interest of promoting arithmetic efficiency and accuracy, the IEEE-754 floating-point standard defined an 80-bit type which was larger than a normal 64-bit double but could be processed faster. The intention was that given an expression like a=b+c+d; it would be both faster and more accurate to convert everything to an 80-bit type, add together three 80-bit numbers, and convert the result to a 64-bit type, than to compute the sum (b+c) as a 64-bit type and then add that to d.

In the interest of supporting the new type, ANSI C defined a new type long double which implementations could have refer to the new 80-bit type or a 64-bit double. Unfortunately, even though the purpose of the IEEE-754 80-bit type was to have all values automatically promote to the new type the way they had been promoted to double, ANSI made it so the new type gets passed to printf or other variadic methods differently from other floating-point types, thus making such automatic promotion untenable.

Consequently, both floating-point types that existed when C was created can use the same %f format specifier, but the long double that was created afterward requires a different %Lf format specifier (with an uppercase L).

Solution 7:[7]

%hhd, %hd, %ld and %lld were added to printf to make the format strings more consistent with scanf, even though they are redundant for printf because of the default argument promotions.

So why wasn't %hf added for float? That's easy: looking at scanf's behaviour, float already has a format specifier. It's %f. And the format specifier for double is %lf.

That %lf is exactly what C99 added to printf. Prior to C99, the behaviour of %lf was undefined (by omission of any definition in the standard). As of C99, it's a synonym for %f.

Solution 8:[8]

Considering scanf has separate format specifiers for float, double, or long double, I don't see why printf and similar functions weren't implemented in a similar way, but that's how C / C++ and the standards ended up.

There can be an issue with the minimum size for a push or pop operation depending on the processor and the current mode, but this could have been handled with default padding, similar to default alignment of local variables or variables in a structure. Microsoft dropped support for 80 bit (10 byte) long doubles when it went from 16 bit to 32 / 64 bit compilers, now treating long doubles the same as doubles (64 bit / 8 byte). They could have padded them to 12 or 16 byte boundaries as needed, but this wasn't done.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Community
Solution 2 Community
Solution 3 Community
Solution 4
Solution 5
Solution 6 supercat
Solution 7
Solution 8 autistic