0

In variadic functions, default argument promotions occur.

6.5.2.2.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. [...]

6.5.2.2.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.

Therefore,

signed char c = 123;
int         i = 123;
float       f = 123;
double      d = 123;

printf("%d\n", i);   // ok
printf("%d\n", c);   // ok, even though %d expects int.
printf("%f\n", d);   // ok
printf("%f\n", f);   // ok, even though %f expects double.

So why is there a printf length modifier for char (hh) and short (h)?


Section number refer to N2176.

ikegami
  • 367,544
  • 15
  • 269
  • 518
  • Try `signed char` or `short` values the the most significant bit set.... – Andrew Henle Jul 01 '21 at 23:33
  • These modifiers tell `printf()` to undo the default promotions, to get the original `char` or `short` value. – Barmar Jul 01 '21 at 23:36
  • @Andrew Henle, Re "*Try `signed char` or `short` values the the most significant bit set...*", Those work perfectly fine with `%d`. `(signed char)-1` and `(short)-1` get promoted to `(int)-1`, which `%d` handles perfectly fine. Did you make a mistake? – ikegami Jul 01 '21 at 23:37
  • @Barmar, Re "*These modifiers tell printf() to undo the default promotions, to get the original char or short value.*", What is there to undo? Integer promotions have no effect on the value – ikegami Jul 01 '21 at 23:40
  • 2
    Symmetry with `scanf()`. Same for `l` in `"%lf"`. – chux - Reinstate Monica Jul 01 '21 at 23:40
  • And the same for `%i` and `%d`. These do the same thing in `printf()`, but differ in `scanf()`. – Barmar Jul 01 '21 at 23:40
  • https://godbolt.org/z/sd4jrj455 – 0___________ Jul 01 '21 at 23:42
  • @0___________, Not sure what your point is? Is it just confirming that integer promotion does occur? – ikegami Jul 01 '21 at 23:47
  • Do the [answers here](https://stackoverflow.com/questions/4586962/what-is-the-purpose-of-the-h-and-hh-modifiers-for-printf) provide any insight? – Steve Summit Jul 01 '21 at 23:49
  • @ikegami Think about precision and C that predated the distinction between `signed int` and `unsigned int` invoking UB if printed with `%x` or simlilar. For example, I'm pretty sure `%x` is not UB when passed a `signed int` in C89. – Andrew Henle Jul 01 '21 at 23:52

4 Answers4

7

Consider this example:

#include <stdio.h>
int main(void)
{
    unsigned short x = 32770;
    printf("%d\n", x) ;  // (1)
    printf("%u\n", x) ;  // (2)
}

On a typical 16-bit implementation, the default argument promotions take unsigned short to unsigned int, whereas on a typical 32-bit implementation, unsigned short becomes int.

So on the 16-bit system (1) is UB and (2) is correct, but on the 32-bit system, (1) is correct and (2) it can be debated whether correct or UB.

Using %hu for printing x works on all systems and you don't have to think about these issues.

A similar example could be constructed for char on systems with sizeof(int) == 1.

M.M
  • 138,810
  • 21
  • 208
  • 365
  • @ikegami see 7.21.6.1/8 . The `u` specifier only has defined behaviour for an `unsigned int` argument. The standard permits a compiler to use different registers for passing `int` than `unsigned int`; when I say "can be debated" I basically mean that people pretend the standard is defective on this point and that they can get away with passing signed ints to `%u` – M.M Jul 01 '21 at 23:54
  • @ikegami line (2) in my program provides `int` argument on a typical 32-bit system. You even quoted the rule in your previous comment, 32-bit `int` can represent all of the values of 16-bit `unsigned short` – M.M Jul 01 '21 at 23:56
  • You should add your answer [here](https://stackoverflow.com/questions/4586962/what-is-the-purpose-of-the-h-and-hh-modifiers-for-printf) Noone mentioned it! – ikegami Jul 01 '21 at 23:59
  • @ikegami maybe a mod could merge the questions, idk – M.M Jul 02 '21 at 00:00
  • I've never seen that happen in SO. We can close as duplicate, which I'm going to have to do even though I think this is the real answer and it's not provided there. – ikegami Jul 02 '21 at 00:01
  • 1
    I have seen it happen – M.M Jul 02 '21 at 00:02
  • If we close this as a dup, I will encourage M.M. to copy-and-paste this excellent answer there. – Steve Summit Jul 02 '21 at 00:31
0

It's for backwards compatibility.

In a draft version of the C89 standard, printing a signed int, short or char with the %xformat specifier is not undefined behavior:

d, i, o, u, x, X The int argument is converted to signed decimal ( d or i ), unsigned octal ( o ), unsigned decimal ( u ), or unsigned hexadecimal notation ( x or X ); the letters abcdef are used for x conversion and the letters ABCDEF for X conversion. The precision specifies the minimum number of digits to appear; if the value being converted can be represented in fewer digits, it will be expanded with leading zeros. The default precision is 1. The result of converting a zero value with an explicit precision of zero is no characters.

This seems to document that pre-standardized C, using format specifiers such as %x for signed values was an existing practice, thus a preexisting code base using h and hh length modifiers likely existed.

Without the h and hh length modifiers, signed char values with a bit pattern 0xFF would be printed on a 32-bit-int system as 0xFFFFFFFF if printed with a simple %X format specifier.

Andrew Henle
  • 32,625
  • 3
  • 24
  • 56
  • @ikegami Find that in the C89 standard: http://port70.net/~nsz/c/c89/c89-draft.html#4.9.6.1 – Andrew Henle Jul 02 '21 at 00:06
  • That error has been fixed in newer version "*The `unsigned int` argument is converted to unsigned octal (o), unsigned decimal (u), or unsigned hexadecimal notation (x or X)*", See also [printf](https://en.cppreference.com/w/c/io/fprintf) – ikegami Jul 02 '21 at 00:07
  • 1
    The C89 text is clearly defective as it only talks about "The int argument" whereas the argument might actually be `unsigned int`. Unless you would like to claim that `printf("%u", 1u);` is UB in C89 of course. – M.M Jul 02 '21 at 00:10
  • I have edited the comment to include actual standard quote. You're using an outdated reference, which is clearly buggy as M.M said. And yes, I do seriously trust cppreference more than you :) – ikegami Jul 02 '21 at 00:10
  • 2
    That is not the C89 standard. It is some draft. The actual ANSI/ISO 9899-1990 standard, in clause 7.9.6.1 (not 4.9.6.1!) has one entry for “`d`, `i`” that says “The `int` argument…” and another for “`o`, `u`, `x`, `X`” that says “The `unsigned int` argument…” – Eric Postpischil Jul 02 '21 at 00:12
  • @EricPostpischil OK, I'll assume you have a better link then? – Andrew Henle Jul 02 '21 at 00:13
  • @Andrew Henle, Trust but verify, which I did. The latest spec (well, N2176) clearly says `unsigned int` – ikegami Jul 02 '21 at 00:13
  • I have a PDF of a scan of the printed standard. It is no longer available at the URL from which I obtained it. – Eric Postpischil Jul 02 '21 at 00:14
  • @AndrewHenle I saved a copy of that link a few years ago and it does not mention `hh` in the 4.9.6.1 section . (The site is currently not serving the whole page for me, it gets to 30% and stops) – M.M Jul 02 '21 at 00:15
  • shit, but you did say it's for backwards compatibility, which I totally missed. Does anyone actually have access to the C89? – ikegami Jul 02 '21 at 00:15
  • @EricPostpischil I'll update to reflect I'm quoting a draft. Because the draft is proof that using `%x` for signed integers did exist for pre-standardized C, so the `h` and `hh` flags would be kept for backwards compatiblity. – Andrew Henle Jul 02 '21 at 00:16
  • @ikegami It's the first line. ;-) – Andrew Henle Jul 02 '21 at 00:17
  • 1
    @EricPostpischil ISO C90 and ANSI C89 are different documents , I wonder whether that splitting of `d`, `i` from the unsigned specifiers occurred between the draft and ANSI C89, or between C89 and C90. I suspect the former, as I have heard C89 and C90 are identical other than paragraph numbering – M.M Jul 02 '21 at 00:19
0

Regarding the hh specifier specifically, it was explicitly added in C99 in order to utilize printing of all the default fixed size types from stdint.h/inttypes.h. C99 mades the types int_leastn_t from 8 to 64, mandatory, so there was a need for corresponding format specifiers.

From the C99 rationale 5.10, §7.19.6.1 (fprintf):

The %hh and %ll length modifiers were added in C99 (see §7.19.6.2).

§7.19.6.2 (fscanf):

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 <inttypes.h> (see 7.18).

Before C99, there was just d, h and l for printing integer types. In C99, a conventional implementation could for example define inttypes.h specifiers as:

#define SCNi8   hh
#define SCNi16   h
#define SCNi32   d
#define SCNi64  ll

And now the default argument promotions becomes the head ache of the printf/scanf implementation, rather than the inttypes.h implementation.

Lundin
  • 195,001
  • 40
  • 254
  • 396
  • The passage you quoted says it was added for `scanf` (nor for `printf` as you claim), and it's very easy to see why it's needed for `scanf` (where the same promotion doesn't occur since pointers are passed as arguments). This does not answer the question. – ikegami Jul 02 '21 at 11:54
  • @ikegami C99 strived to make printf and scanf consistent. They fixed the `%lf` specifier for printf for example. After fixing that, what would they add new specifiers to scanf but not to printf? Luckily they didn't. Like it or not, this _was_ the reason `hh` got added to the language. – Lundin Jul 02 '21 at 12:01
  • That *would* be an answer (though claiming it's for consistency is contradicted by the fact that they are actually needed with printf in some situations as pointed out by the earlier answers.) I commented on what you *did* answer. – ikegami Jul 02 '21 at 12:03
  • @ikegami Added reference for printf. – Lundin Jul 02 '21 at 12:39
0

They are not there for printf() usage, but for scanf() to be able to use references to short integers and char integers. For uniformity and completeness, they are accepted for printf() functions, but they are undistinguisable, as the vaarg parameters of printf() are promoted to int for all parameters that are of types short and char integer values. So they are equivalent in printf() but not in scanf() and friends.

Luis Colorado
  • 10,974
  • 1
  • 16
  • 31