Variadic Functions

All of the formatted i/o functions are variadic, meaning they accept a variable quantity of variably typed parameters. Before discussing formatted i/o, it is necessary to understand the basics of variadic functions.

Declaration

Variadic functions are declared like normal functions with a trailing ellipsis (…) in their parameter list, and must have at least one named parameter preceding the ellipsis,

int sum(int count, ...);

Usage

The header stdarg.h exposes a portable interface for iterating over these additional arguments. It consists of a type, va_list which is a handle to the list of variadic arguments, and four macros, va_start for initializing the va_list object, va_copy for duplicating it, va_arg for extracting the next argument, and va_end for destroying it.

For example, a function which sums an arbitrary quantity of integers might be written as,

int sum(int count, ...)
{
  va_list ap;
  va_start(ap, count); /* Initialize the variadic argument list */
  int result = 0;
  for (int i = 0; i < count; ++i) {
    result += va_arg(ap, int); /* Extract next variadic argument, of type int */
  }
  va_end(ap); /* Destroy the variadic argument list */
  return result;
}

Library Implementation

The exact implementation of variadic arguments depends on the calling convention of the execution environment; a canonical implementation places the arguments on the stack, in order, so that the variadic arguments follow the last named parameter. The variadic arguments are iterated over using pointer arithmetic, starting from the address immediately after the last named parameter. This requires knowing the size and type of each variadic argument, which are deduced at runtime–typically from some information passed in a named parameter, like the format string of a formatted i/o function, as will be explained later.

A possible implementation of the stdarg.h header might look like,

typedef void *va_list;
#define va_start(ap, argN) ((ap) = (&(argN) + 1))
#define va_arg(ap, type) ((ap) = ((type*)(ap) + 1), *((type*)(ap)) - 1))
#define va_copy(dest, src) ((dest) = (src))
#define va_end(ap) ((ap) = 0)

In the above example, va_list is simply a type alias for the universal pointer, void*. The va_start macro takes the address of the last named parameter, argN, and points ap to the address immediately following it–i.e. the address of the first variadic argument on the stack. The va_arg macro first advances ap to point to the next argument, and then evaluates the previously pointed-at location, cast to the requested type; this is an example of the use of the comma operator, ,, to evaluate two sub-expressions while discarding the result of the first. The va_copy macro simply copies the pointer value from src to dest, and the va_end macro simply sets ap to a null pointer.

Potential Pitfalls

Notice, critically, that our example va_arg casts ap to a pointer to type (type*) both to advance ap and to return a value of the requested type. If type does not align with the actual type of the variadic argument, the behavior is undefined; this is different from a regular function call, where the compiler knows the argument type and the parameter type, and automatically converts the value being passed in. Here, the compiler does not know the expected parameter type, since it is calculated at runtime, and is unable to automatically perform the conversion if the types differ. Therefore, the caller is responsible for ensuring that variadic arguments are exactly the expected type.

For example, calling the sum function above, with an incorrect count or with non-int variadic arguments, causes unexpected behavior. In some situations, it might appear to work correctly, and in others, subtle bugs might appear. For example, sum(2, 1, 1.0) will generally produce a garbage value, since the floating point value 1.0 is directly interpreted as an integer, which has a completely different object representation.

Arguments which differ from the types expected by a variadic function must be explicitly cast by the caller to the appropriate types. In the example above, sum(2, 1, (int) 1.0) would give the desired result.

As a note, the trailing arguments to a variadic function undergo default argument promotions–integer type values with rank lower than int are promoted to int or unsigned int, and float values are converted to double. For example, the following is technically correct because the char argument is promoted to int when the printf function is called with a format specifier for an int type argument:

char c = 'A';
printf("%d\n", c); /* 65 */

An argument could be made for either 1) explicitly casting c to an intprintf(%dn", (int)c);; or 2) using the length modifier hhprintf("%hhdn", c);; in order to promote clarity of intent. On the other hand, it can be argued that either solution makes the code more cumbersome to read. In actual practice, the former argument usually prevails.

Forwarding Wrappers

There is no way for a variadic function to forward all of its variadic arguments to another variadic function; however, a variadic function can pass an initialized va_list object to another function. For this reason, it is common to separate the interface from the implementation by writing two functions–an implementation that takes a va_list parameter, and a variadic wrapper function that provides an interface. Typically, the implementation function is given the same name as the wrapper function, with a ‘v’ prefix:

int vsum(int count, va_list ap)
{
  int result = 0;
  for (int i = 0; i < count; ++i) result += va_arg(ap, int);
  return result;
}

int sum(int count, ...)
{
   va_list ap;
   va_start(ap, count);
   int result = vsum(count, ap);
   va_end(ap);
   return result;
}

This way, the implementation can be re-used in implementing different variadic interfaces.