1

How exactly do variadic functions treat numeric constants? e.g. consider the following code:

myfunc(5, 0, 1, 2, 3, 4);

The function looks like this:

void myfunc(int count, ...)
{
}

Now, in order to iterate over the single arguments with va_arg, I need to know their sizes, e.g. int, short, char, float, etc. But what size should I assume for numeric constants like I use in the code above?

Tests have shown that just assuming int for them seems to work fine so the compiler seems to push them as int even though these constants could also be represented in a single char or short each.

Nevertheless, I'm looking for an explanation for the behaviour I see. What is the standard type in C for passing numeric constants to variadic functions? Is this clearly defined or is it compiler-dependent? Is there a difference between 32-bit and 64-bit architecture?

Thanks!

Andreas
  • 9,245
  • 9
  • 49
  • 97

2 Answers2

3

I like Jonathan Leffler's answer, but I thought I'd pipe up with some technical details, for those who intend to write a portable library or something providing an API with variadic functions, and thus need to delve in to the details.

Variadic parameters are subject to default argument promotions (C11 draft N1570 as PDF; section 6.5.2.2 Function calls, paragraph 6):

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

Floating-point constants are of type double, unless they are suffixed with f or F (as in 1.0f), in which case they are of type float.

In C99 and C11, integer constants are of type int if they fit in one; long (AKA long int) if they fit in one otherwise; of long long (AKA long long int) otherwise. Since many compilers assume an integer constant without a size suffix is a human error or typo, it is a good practice to always include the suffix if the integer constant is not of type int.

Integer constants can also have a letter suffix to denote their type:

  • u or U for unsigned int

  • l or L for long int

  • lu or ul or LU or UL or lU or Lu or uL or Ul for unsigned long int

  • ll or LL or Ll or lL for long long int

  • llu or LLU (or ULL or any of their uppercase or lowercase variants) for unsigned long long int

The integer promotion rules are in section 6.3.1.1.

To summarize the default argument promotion rules for C11 (there are some additions compared to C89 and C99, but no significant changes):

  • float are promoted to double

  • All integer types whose values can be represented by an int are promoted to int. (This includes both unsigned and signed char and short, and bit-fields of types _Bool, int, and smaller unsigned int bit-fields.)

  • All integer types whose values can be represented by an unsigned int (but not an int) are promoted to unsigned int. (This includes unsigned int bit fields that cannot be represented by an int (of CHAR_BIT * sizeof (unsigned int) bits, in other words), and typedef'd aliases of unsigned int, but that's it, I think.)

  • Integer types at least as large as int are unchanged. This includes types long/long int, long long/long long int, and size_t, for example.

There is one 'gotcha' in the rules that I'd like to point out: "signed to unsigned is okay, unsigned to signed is iffy":

  • If the argument is promoted to a signed integer type, but the function obtains the value using the corresponding unsigned integer type, the function obtains the correct value using modulo arithmetic.

    That is, negative values will be as if they were incremented by (1 + maximum representable value in the unsigned integer type), making them positive.

  • If the argument is promoted to an unsigned integer type, but the function obtains the value using the corresponding signed integer type, and the value is representable in both, the function obtains the correct value. If the value is not representable in both, the behaviour is implementation-defined.

    In practice, almost all architectures do the opposite of above, i.e. the signed integer value obtained matches the unsigned value substracted by (1 + the largest representable value of the unsigned integer type). I've heard that some strange ones may signal integer overflow or something similarly weird, but I have never gotten my mitts on such machines.

The man 3 printf man page (courtesy of the Linux man pages project) is quite informative, if you compare the above rules to printf specifiers. The make_message() example function at the end (C99, C11, or POSIX required for vsnprintf()) should also be interesting.

Community
  • 1
  • 1
Nominal Animal
  • 38,216
  • 5
  • 59
  • 86
  • Thanks for the answer! I've justed a follow-up here: http://stackoverflow.com/questions/40330749/why-does-my-variadic-function-work-with-both-int-and-long-long – Andreas Oct 30 '16 at 15:43
1

When you write 1, that is an int constant. There is no other type that the compiler is allowed to use. If there is a non-variadic prototype for the function that demands a different type, the compiler will convert the integer 1 to the appropriate type, but on its own, 1 is an int constant. So, in your example, all 6 arguments are int.

You have to know the types of the arguments somehow before the called variadic function processes them. With the printf() family of functions, the format string tells it what to expect; similarly with the scanf() family of functions.

Note that the default conversions apply to the arguments corresponding to the ellipsis of a variadic function. For example, given:

char c = '\007';
short s = 0xB0hD;
float f = 3.1415927;

a call to:

int variadic_function(const char *, ...);

using:

int rc = variadic_function("c s f", c, s, f);

actually converts both c and s to int, and f to double.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
  • Thanks for the answer! I've justed a follow-up here: http://stackoverflow.com/questions/40330749/why-does-my-variadic-function-work-with-both-int-and-long-long – Andreas Oct 30 '16 at 15:43