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.

Was it helpful?

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

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:

  1. if size_t is in range of int, promote to int
  2. if size_t is in range of unsigned int, promote to unsigned int
  3. otherwise, size_t has greater conversion rank (and thus width) than unsigned 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 of int
  • size_t is not in range of int, but in range of unsigned int without actually being unsigned 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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top