View this question from the perspective of someone implementing printf
.
Since the arguments of printf
are passed through an ellipsis (...
), they get integer promoted. I know that char
, short
and int
get promoted to int
while long long
does not get promoted. Similarly for their unsigned
counterparts.
This implies that when reading the varargs, va_arg(args, int)
should be used for char
, short
and int
while va_arg(args, long long)
should be used for long long
.
My question is, do long
and size_t
get promoted, and if they do, to what? There are many sources on the internet on integer promotion, but I haven't seen any that talk about these types.
P.S. I would appreciate a reference to the standard.
The integer conversion rank of long
is required to be greater than the rank of int
(6.3.1.1p1), so va_arg(args, long)
is required even if long
has the same representation (and precision) as int
. Note that on most 64-bit platforms, long
is 64-bit; Windows (an LLP64 platform) is an exception.
size_t
is required to be an unsigned integer type (6.5.3.4p5, 7.19p2) and is recommended to have an integer conversion rank no greater than that of long int
(7.19p4); it is required to have a precision of at least 16 bits (7.20.3p2, minimum value of SIZE_MAX
). It is not required to be a (typedef to a) standard integer type, although it is allowed to be.
There are then three possibilities for the integer conversion rank of size_t
:
int
, so a size_t
argument will be promoted to either int
(if the precision of size_t
is less than that of int
) or unsigned int
(if the two types have the same precision). In either case you would need to write va_arg(args, unsigned int)
(even if the size_t
argument is promoted to int
, using the equivalent unsigned type is allowed by 7.16.1.1p2).int
, i.e. size_t
is the same type as unsigned int
. In this case either va_arg(args, unsigned int)
or va_arg(args, size_t)
are allowed.int
. In this case va_arg(args, size_t)
must be used.Note that either of 1 and 3 can obtain even if the precision of size_t
is the same as that of int
.
This means that to extract a size_t
parameter using va_arg
, it is necessary to know or infer the integer conversion rank of size_t
. This can be done using a type-generic macro (6.5.1.1):
#define va_arg_size_t(args) _Generic((+(sizeof(0))), \
int: (size_t) va_arg((args), unsigned int), \
unsigned int: (size_t) va_arg((args), unsigned int), \
default: va_arg((args), size_t))
If size_t
is promoted to int
by the unary plus operator as used above, then we extract an unsigned int
; if size_t
is promoted to unsigned int
, or is a typedef to unsigned int
, then we extract an unsigned int
; if it is not promoted and is a distinct type from unsigned int
, then we hit the default
block. We can't supply size_t
itself as an option, as that would conflict if size_t
were a typedef for unsigned int
.
Note that this is a problem not restricted to size_t
, ptrdiff_t
and wchar_t
have the same issue (for the latter, wint_t
can hold any wchar_t
value and is not subject to promotion, but there is no guarantee that wchar_t
is promoted to wint_t
, unlike the guarantee that char
is promoted to int
). I'd suggest that the standard needs to introduce new types spromo_t
, ppromo_t
and wpromo_t
, and similarly for the types in stdint.h
. (Sure, you can use _Generic
as above, but it's a pain in the neck.)