Integer promotion for `long` and `size_t` when sent through ellipsis?
-
07-07-2021 - |
Question
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.
Solution
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
:
- It is less than that of
int
, so asize_t
argument will be promoted to eitherint
(if the precision ofsize_t
is less than that ofint
) orunsigned int
(if the two types have the same precision). In either case you would need to writeva_arg(args, unsigned int)
(even if thesize_t
argument is promoted toint
, using the equivalent unsigned type is allowed by 7.16.1.1p2). - It is the same as that of
int
, i.e.size_t
is the same type asunsigned int
. In this case eitherva_arg(args, unsigned int)
orva_arg(args, size_t)
are allowed. - It is greater than that of
int
. In this caseva_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.)
OTHER TIPS
C says (emphasis mine):
(C99, 6.3.1.1p2) " The following may be used in an expression wherever an int or unsigned int may be used:
— An object or expression with an integer type whose integer conversion rank is less than or equal to the rank of int and unsigned int.
— A bit-field of type _Bool, int, signed int, or unsigned int.
If an int can represent all values of the original type, the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions.48) All other types are unchanged by the integer promotions."
Arguments of type long
won't be promoted. The integer promotions relevant to size_t
can be summarized as follows:
- if
size_t
is in range ofint
, promote toint
- if
size_t
is in range ofunsigned int
, promote tounsigned int
- otherwise,
size_t
has greater conversion rank (and thus width) thanunsigned int
and no promotion happens
The trivial case is if size_t
is an alias for or has greater width than unsigned int
. In these cases, no promotions happen and you can use size_t
to read the variadic argument.
The edge cases are
size_t
is in range ofint
size_t
is not in range ofint
, but in range ofunsigned int
without actually beingunsigned int
The latter can happen if int
contains padding of if size_t
is an extended integer type of same width as unsigned int
.
Assuming integer representations do not contain padding bits, you can cover both edge cases by reading an unsigned int
variadic argument, which will work on all reasonable implementations of the C language despite possibly being undefined behaviour.