Search code examples
cinteger-promotion

Integer promotion for `long` and `size_t` when sent through ellipsis?


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:

    1. It is less than that of 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).
    2. It is the same as that of 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.
    3. It is greater than that of 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.)