10

7.16.1.1 2 describes va_arg as following (emphasis mine):

If there is no actual next argument, or if type is not compatible with the type of the actual next argument (as promoted according to the default argument promotions), the behavior is undefined, except for the following cases:

  • one type is a signed integer type, the other type is the corresponding unsigned integer type, and the value is representable in both types;
  • one type is pointer to void and the other is a pointer to a character type.

Now to my understanding and it seems that 6.5.2.2 (function calls) does not contradict me, though I might be wrong, the default promotions are:

  • char to either int or unsigned (implementation specified)
  • signed char to int
  • unsigned char to unsigned
  • short to int
  • unsigned short to unsigned
  • float to double

This is all fine and dandy when you know the exact underlying types passed to the va_list (except for char, which AFAIK is impossible to retrieve portably because its signedness is implementation specified).

It gets more complicated when you're expecting types from <stdint.h> to be passed to your va_list.

  • int8_t and int16_t, deducting through logical limit observations, are guaranteed to be promoted or already be of type int. However it's very dubious to rely on my original "logical" limit observations, so I'm seeking your (and the standard's) confirmation on this deduction (I may be missing some corner cases I'm not even aware of).
  • the same holds for uint8_t and uint16_t, except the underlying type is unsigned
  • int32_t may or may not be promoted to int. It may be larger than , smaller than or exactly the same as int. Same holds for uint32_t but for unsigned. How to portably retrieve int32_t and uint32_t passed to va_list? In other words, how to determine if int32_t (uint32_t) has been promoted to int(unsigned)? In yet other words, how to determine whether I should use va_arg(va, int) or va_arg(va, int32_t) to retrieve int32_t passed to the variadic function without invoking undefined behaviour on any platform?
  • I believe the same questions are valid for int64_t and uint64_t.

This is a theoretical (standard-only concerned) question, with a presumption that all exact-width types in <stdint.h> are present. I'm not interested in "what's true in practice" type of answers, because I believe I already know them.

EDIT

One idea that I have in mind is to use _Generic to determine the underlying type of int32_t. I'm not sure how exactly would you use it though. I'm looking for better (easier) solutions.

MarkWeston
  • 740
  • 4
  • 15
  • There is little to no chance that `int32_t` is smaller than `int`. That would be a rather strange implementation, as a 64-bit `int` would mean that `short` is either 16 or 32 bit, leaving the other width *without* a corresponding "native" type. – DevSolar Dec 28 '17 at 13:34
  • @DevSolar: There are truly byzantine architectures out there, especially in the embedded space. Still, a system not promoting to int32_t to int_fast32_t to would be particularly weird. Personally I wouldn't bother and just settle for asserting PRIu32, unless there is an obvious solution. – doynax Dec 28 '17 at 13:40
  • 1
    One not-so-crazy solution would be to use `autoconf` to discover the compiler behavior and a macro or two. – rodrigo Dec 28 '17 at 14:11
  • 2
    The promotions do not respect signedness. Per 6.3.1.2 2, “If an **int** can represent all values of the original type (as restricted by the width, for a bit-field), the value is converted to an **int**; otherwise, it is converted to an **unsigned int**.” So a `uint8_t` would become `int`, not `unsigned int`. – Eric Postpischil Dec 28 '17 at 14:12
  • `unsigned char` and `unsigned short` are promoted to `int` if it can represent all values of the source type, and `unsigned int` otherwise. – T.C. Dec 29 '17 at 09:01

3 Answers3

7
#define IS_INT_OR_PROMOTED(X) _Generic((X)0 + (X)0, int: 1, default: 0)

Usage:

int32_t x = IS_INT_OR_PROMOTED(int32_t) ? 
              (int32_t)va_arg(list, int) : 
              va_arg(list, int32_t);

With gcc on my PC the macro returns 1 for int8_t, int16_t and int32_t, and 0 for int64_t.

With gcc-avr (a 16-bit target) the macro returns 1 for int8_t and int16_t, and 0 for int32_t and int64_t.

For long the macro returns 0 regardless of whether sizeof(int)==sizeof(long).

I don't have any targets with 64-bit ints but I don't see why it wouldn't work on such a target.

I'm not sure this will work with truly pathological implementations though Actually I'm pretty sure now it will work with any conforming implementation.

n. m. could be an AI
  • 112,515
  • 14
  • 128
  • 243
  • Honestly I can't see why wouldn't it work with any conformant implementation. This may be the answer unless someone can provide a caveat. – MarkWeston Dec 28 '17 at 16:22
  • Pile the up votes on this one, people (unless somebody finds a flaw). Esoteric knowledge applied to solve an obscure but valid problem deserves to be recognized. – Eric Postpischil Dec 29 '17 at 02:44
  • This is excellent and removes one of the last blocking issues I had with more widespread use of the `` types. – BeeOnRope Dec 30 '17 at 19:54
0

Indeed there is no good way to do this. I consider the canonical answer to be "don't do this". Beyond not passing such types as arguments to variadic functions, avoid even using them as "variables" and only use them as "storage" (in arrays and structs that exist in large quantities). Of course it's easy to make a mistake and pass such an element/member as an argument to your variadic function, so that's not very satisfying.

Your idea with _Generic only works if these types aren't defined with implementation-pecific extended integer types your code is unaware of.

There's an awful but valid approach involving passing the va_list to vsnprintf with the right "PRI*" macro, then parsing the integer from the string, but after doing this the list is in a state where you can't use it again, so if only works for final argument.

Your best bet is probably trying to find a formula for "does this type get promoted by default promotions?" You can easily query whether the the max value of the type exceeds INT_MAX or UINT_MAX but this still doesn't help the formal correctness if there's a spurious extended integer type with same range.

R.. GitHub STOP HELPING ICE
  • 208,859
  • 35
  • 376
  • 711
  • What is the problem with a spurious extended integer type? If `int32_t` values are a subset of `int` values, `int32_t` is promoted to `int`, so `va_arg(list, int)` works. If not, `int32_t` is not promoted, so `va_arg(list, int32_t)` works. What am I missing? – Eric Postpischil Dec 28 '17 at 14:09
  • @EricPostpischil: If `int` is 16-bits, then `va_arg(list, int)` would be UB; if `int` is 64-bits, then `va_arg(list, int32_t)` is UB; either way you cannot write a code that works every time. – rodrigo Dec 28 '17 at 14:13
  • @rodrigo: The code would be `INT_MIN <= INT32_MIN && INT32_MAX <= INT_MAX ? va_arg(list, int) : va_arg(list, int32_t)` (or an equivalent decision using `#if`). So, yes, we can write source code that uses `int` when `int32_t` is promoted to `int` and that uses `int32_t` when it is not promoted to `int`. But R.. seems to indicate there is some problem with this. – Eric Postpischil Dec 28 '17 at 14:33
  • @R.. If `INT32_MAX` is `>`, `<` or `==` to `INT_MAX`, doesn't that guarantee that the rank of `int32_t` is higher, lower or equal to that of `int`? Where's the caveat? – MarkWeston Dec 28 '17 at 14:41
  • You can use `va_copy` to circumvent the problem of the argument list being unusable afterwards. – fuz Dec 28 '17 at 14:55
  • 2
    @MarkWeston: When the range of `int32_t` and the range of `int` are equal, that does not make the conversion rank equal. These two bullets complicate things: **The rank of long long int shall be greater than the rank of long int, which shall be greater than the rank of int, which shall be greater than the rank of short int, which shall be greater than the rank of signed char.** and **The rank of any standard integer type shall be greater than the rank of any extended integer type with the same width.** – Ben Voigt Dec 28 '17 at 15:57
  • @fuz: But then you can't get past the troublesome arg to read the next one. – R.. GitHub STOP HELPING ICE Dec 29 '17 at 00:45
0

Regarding the #if and <limits.h> solution, I found this (6.2.5.8):

For any two integer types with the same signedness and different integer conversion rank (see 6.3.1.1), the range of values of the type with smaller integer conversion rank is a subrange of the values of the other type.

And 6.3.3.1 states (emphasis mine):

Every integer type has an integer conversion rank defined as follows:

  • No two signed integer types shall have the same rank, even if they have the same representation.
  • The rank of a signed integer type shall be greater than the rank of any signed integer type with less precision.
  • The rank of long long int shall be greater than the rank of long int, which shall be greater than the rank of int, which shall be greater than the rank of short int, which shall be greater than the rank of signed char.
  • The rank of any unsigned integer type shall equal the rank of the corresponding signed integer type, if any.
  • The rank of any standard integer type shall be greater than the rank of any extended integer type with the same width.
  • The rank of char shall equal the rank of signed char and unsigned char.
  • The rank of _Bool shall be less than the rank of all other standard integer types.
  • The rank of any enumerated type shall equal the rank of the compatible integer type (see 6.7.2.2).
  • The rank of any extended signed integer type relative to another extended signed integer type with the same precision is implementation-defined, but still subject to the other rules for determining the integer conversion rank.
  • For all integer types T1, T2, and T3, if T1 has greater rank than T2 and T2 has greater rank than T3, then T1 has greater rank than T3.

And this is what 6.5.2.2 6 says (emphasis mine):

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. If the number of arguments does not equal the number of parameters, the behavior is undefined. If the function is defined with a type that includes a prototype, and either the prototype ends with an ellipsis (, ...) or the types of the arguments after promotion are not compatible with the types of the parameters, the behavior is undefined. If the function is defined with a type that does not include a prototype, and the types of the arguments after promotion are not compatible with those of the parameters after promotion, the behavior is undefined, except for the following cases:

  • one promoted type is a signed integer type, the other promoted type is the corresponding unsigned integer type, and the value is representable in both types;

  • both types are pointers to qualified or unqualified versions of a character type or void

Based on these observations I'm lead to believe that

#if INT32_MAX < INT_MAX
    int32_t x = va_arg(va, int);
#else
    int32_t x = va_arg(va, int32_t);

This is because if the range of int32_t can't contain the range of int, then the range of int32_t is a subrange of int, which means that the rank of int32_t is lower than that of int, and this means that the integer promotion is performed.

On the other hand, if the range of int32_t can contain the range of int, then the range of int32_t is the range of int or a superset of the range of int, and thus the rank of int32_t is greater or equal than the rank of int, which means that the integer promotion is not performed.

EDIT

Corrected the test, according to the comments.

#if INT32_MAX <= INT_MAX && INT32_MIN >= INT_MIN
    int32_t x = va_arg(va, int);
#else
    int32_t x = va_arg(va, int32_t);

EDIT 2:

I'm now specifically interested in this case:

  • int is 32-bit one's complement integer.
  • int32_t is 32-bit two's complement integer (extended type)
  • the width (same as precision?) is the same
  • but because "The rank of any standard integer type shall be greater than the rank of any extended integer type with the same width." the rank of int is higher than that of int32_t
  • this means the integer promotion from int32_t to int must be performed
  • even though int can't represent all values in int32_t (specifically, it can't represent INT32_MIN) What happens? Or am I missing something?
MarkWeston
  • 740
  • 4
  • 15
  • If `int32_t` is an extended integer type the same size as `int`, you would need `<=`, because **The rank of any standard integer type shall be greater than the rank of any extended integer type with the same width.** – Ben Voigt Dec 28 '17 at 15:53
  • 1
    @EricPostpischil: But `int32_t` *can* also be a typedef for a standard integer type, it isn't required to be an extended type... In case of `typedef long int int32_t;` then promotion will not take place. Hence my comment is couched in conditionals. – Ben Voigt Dec 28 '17 at 15:54
  • @BenVoigt: I concur, the case where `int32_t` is `long int` seems to be problematic. – Eric Postpischil Dec 28 '17 at 19:15