Variadic Functions

Function Arguments

In the olden days, (K&R C), certain function arguments were promoted:

These are called the default argument promotions. This was done because you weren't required to prototype your functions before using them. (In C, you still aren't required as it's only a warning if you don't.) So, the rule is:

If the compiler doesn't know the type of the formal parameters when it generates code to call a function, the default argument promotions are performed.

Although this is mostly irrelevant for any new code you write, this will become an important issue when you use variable argument lists. (like printf or scanf)

From the C Standard 6.3.2.2:

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:


Function Refresher: Three variations of a swap function:

Swap two integersSwap two addressesSwap two integers
void swap1(int a, int b)
{
  int temp;

  temp = a; 
  a = b;
  b = temp;
}
void swap2(int *a, int *b)
{
  int *temp;

  temp = a;
  a = b;
  b = temp;
}
void swap3(int *a, int *b)
{
  int temp;

  temp = *a;
  *a = *b;
  *b = temp;
}

All of the swap functions are passing their arguments by value. How does it work?

int x = 5, y = 8;

swap1(x, y);   /* Doesn't work right                                       */
swap1(5, 8);   /* Won't work either and would be an abomination if it did! */
swap2(&x, &y); /* Still doesn't work quite right                           */
swap2(5, 8);   /* Compile error                                            */
swap3(&x, &y); /* Ok, works as expected                                    */
What does the following code print out?
Version 1Version 2
void change_pointer1(char *ptr)
{
  ptr = "Bye";
}

int main(void)
{
  char *p = "Hello";
  printf("p = %s\n", p);
  
  change_pointer1(p);
  printf("p = %s\n", p);
  
  return 0;
}
void change_pointer2(char **ptr)
{
  *ptr = "Bye";
}

int main(void)
{
  char *p = "Hello";
  printf("p = %s\n", p);
  
  change_pointer2(&p);
  printf("p = %s\n", p);
  
  return 0;
}

It is ABSOLUTELY ESSENTIAL that you understand EXACTLY why the program above works the way it does. This is the whole key to understanding pointers and function parameters. Two things that you must master to be successful C and C++ programmers.

Variable Argument Lists

How does the compiler deal with printf and these calls? In other words, what is the signature of printf?

char c = 'A';
int age = 20;
float pi = 3.1415F;

printf("c = %c, ", c);
printf("age = %i, ", age);
printf("pi = %f\n", pi);
printf("pi = %f, age = %i\n", pi, age);
printf("c = %c, age = %i, pi = %f\n", c, age, pi);
It seems that printf has multiple signatures. Actually, it has just one (simplified):
int printf(const char *format, ...);
This syntax is for functions with a variable argument list. Question: "How does printf know how many arguments were passed in?"

Given this code:

char ch = 'A';  /* 'A' is ASCII 65 */
int age = 20;
float pi = 3.1415F;
const char *format = "c = %c, age = %i, pi = %f\n";

printf(format, ch, age, pi);  
We can visualize passing the arguments to printf something like this:
Because printf "knows" where to find the format argument on the stack, it can easily find all of the other arguments. The format string specifies the order and type (size) of the arguments.

Now can you see why bad things happen if the format specifier doesn't match the type of the argument?

Using %f with chUsing %f with ageUsing %i with pi

The shaded area shows which bytes are going to be read from the stack according to the format specifier given to printf.

A First Example

Let's create a function that takes the average of "a bunch" of integers. The size of "a bunch" can vary with each call. Here's what the prototype for our function might look like:

double average(int count, ...);
We would like to be able to call average like this:
double ave1, ave2, ave3;

ave1 = average(5, 1, 2, 3, 9, 10);
ave2 = average(7, 5, 8, 9, 2, 4, 4, 5);
ave3 = average(3, 11, 2, 10);
printf("ave1 = %f, ave2 = %f, ave3 = %f\n", ave1, ave2, ave3);
Output:
ave1 = 5.000000, ave2 = 5.285714, ave3 = 7.666667
In the example above, the first argument to the average function was a count of how many numbers that the function should expect. So, how do we write such a function?

Here is one way:

double average(int count, ...)
{
    /* va_list is usually a typedef for char * */
  va_list args;
  int i, total = 0;

    /* Initialize pointer to variable-length list.
     * args points into the stack after count's address.
     */
  va_start(args, count);

    /* Sum all values in list */
  for (i = 0; i < count; i++)
  {
    int next = va_arg(args, int); /* Next integer */
    total += next;
  }

    /* Reset, required cleanup (may free memory, may do nothing) */
  va_end(args); 

  return (double)total / count;
}
In the example above, we specified the number of arguments that the function should expect. We don't have to do it this way. Another way is to use a sentinel value. We can call sum like this:
int sum1, sum2, sum3;

sum1 = sum(2, 4, 5, 7, 8, 0);
sum2 = sum(12, 17, 28, 0);
sum3 = sum(21, 41, 25, 17, 18, 3, 8, 23, 0);
printf("sum1 = %i, sum2 = %i, sum3 = %i\n", sum1, sum2, sum3);

  /* Need to be careful! (What if sum1, or sum2, or sum3 is 0?) */
printf("sum = %i\n", sum(sum1, sum2, sum3, 0));
Output:
sum1 = 26, sum2 = 57, sum3 = 156
sum = 239
Implementation:
int sum(int first, ...)
{
  va_list args;
  int total = 0;
  int next = first;  /* first value is now data */

    /* Initialize pointer to variable-length list */
  va_start(args, first);  

    /* Sum all values in the list (0 is the sentinel) */
  while (next != 0)
  {
    total += next;
    next = va_arg(args, int);  /* Next value */
  }
  
    /* Reset, required cleanup */
  va_end(args); 

  return total;
}
Our sentinel value is 0. Choosing a good sentinel value is important and should be something that can never be a valid value in the list of args. A possible better choice than zero might be one of these:
INT_MIN, which has a value of -2147483648.
INT_MAX, which has a value of +2147483647.
Both INT_MIN and INT_MAX are defined in stdint.h.

Realize also that, the examples used literal constants, but the inputs to the functions could have all been variables (that aren't known until runtime).


Suppose we wanted to write a function to print any number of strings. We'd use it like this:

print_strings("one", "two", "three", "four", NULL);
print_strings("one", NULL);
print_strings("one", "two", NULL, "four", NULL);
print_strings(NULL);
And expect to see:
one  two  three  four
one
one  two
[empty line here]
The code would look something like this:

void print_strings(const char *first, ...)
{
  va_list args;
  const char *next = first;  /* first value */

    /* Initialize pointer to args on the stack */
  va_start(args, first);  

    /* Print all strings */
  while (next != NULL)
  {
    printf("%s  ", next);
    next = va_arg(args, const char *);  /* Next value */
  }
  printf("\n");
  
    /* Reset, required cleanup */
  va_end(args); 
}
The va_ macros are defined in stdarg.h and might look like this (32-bit system):
typedef char *  va_list;

#define _INTSIZEOF(n)   ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )

#define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap)      ( ap = (va_list)0 )
It is not necessary to understand the details in order to use them. They are compiler-dependent, so you can't rely on any particular implementation.

Since you use the ellipsis to "prototype" your function, the compiler doesn't know the types (sizes) of the parameters. The default argument promotion will occur so you will have to remember to retrieve int and double (never char, short, or float) in the function.

For example, this function won't work:
float f_average(int count, ...)
{
  va_list args; /* char* */
  int i = 1;
  float total = 0;
  float next;

    /* Initialize pointer to first parameter in var list */
  va_start(args, count);

    /* Sum all values in list */
  for (i = 0; i < count; i++)
  {
    next = va_arg(args, float); /* Next float (+4 bytes) */
    total += next;
  }

    /* Reset, required cleanup */
  va_end(args); 

  return total / count;
}
no matter how you try to use it:
  /* Try to pass floats */
float f1 = f_average(5, 1.0F, 2.0F, 3.0F, 9.0F, 10.0F);

  /* Try to pass doubles */
float f2 = f_average(5, 1.0, 2.0, 3.0, 9.0, 10.0);

  /* Displays: f1 = 0.775000, f2 = 0.775000 */
printf("f1 = %f, f2 = %f\n", f1, f2);
This is because: Fortunately, some compilers will detect this error and warn you about it. This is the output from gcc at all warning levels:
In function 'f_average':
warning: 'float' is promoted to 'double' when passed through '...'
     next = va_arg(args, float); // Next float (+4 bytes)
                         ^
note: (so you should pass 'double' not 'float' to 'va_arg')
note: if this code is reached, the program will abort
This is the output from Clang at all warning levels:
warning: second argument to 'va_arg' is of promotable type 'float'; this va_arg has
         undefined behavior because arguments will be promoted to 'double' [-Wvarargs]
    next = va_arg(args, float); // Next float (+4 bytes)
                        ^~~~~
Sadly, Microsoft's compiler says nothing, even with warnings set to the maximum.


Of course, to add up a "bunch" of numbers you could have more easily done it with arrays:
int sum2(int *array, int size)
{
  int i, sum = 0;
 
  for (i = 0; i < size; i++)
    sum += array[i];

  return sum;
}
Usage:
int array[] = {21, 41, 25, 17, 18, 3, 8, 23};
printf("sum = %i\n", sum2(array, 8));
But this would require you to create an array instead of just sending all of the constants (or variables) to the function. Also, what if all of the types were not integers but include floating-point numbers as well? You can't mix types with an array.

Another Example: Mixing Types

The previous examples all passed the same data types to the functions. Suppose we want to write a function that can calculate the average of bunch of different types. We will assume the return value will be a double, since it's likely to include a fractional part. We need some way communicating the types of the arguments to the variadic function. Let's borrow the idea from printf. That is, we will provide a sort of "format string".

We will be able to pass integral types and floating-point types. For example, this is an example of such a call (on a 32-bit system):

double ave = average2("ifif", 1, 2.0F, 3L, 4.0);
printf("ave is %f\n", ave);
Output:
ave is 2.500000
The type string is just a NUL-terminated string of characters. The only valid characters are i, for integral values and f for floating-point values. Notice that we no longer need to provide the actual count of arguments. The count is implied by the length of the type string. (This is similar to how printf knows how many arguments were provided.)

This is what the function might look like:

double average2(const char *types, ...)
{
  va_list args;
  double total = 0;
  int count = 0;

    /* Initialize pointer to variable-length list       */
  va_start(args, types);

    /* Sum all values in list */
  while (*types)
  {
    double next = 0;

      /* Only supports two types, no error checking is done */
    if (*types == 'i')
      next = va_arg(args, int); 
    else if (*types == 'f')
      next = va_arg(args, double);

    total += next; 
    count++;      

      /* Next format character */
    types++; 
  }

    /* Reset, required cleanup */
  va_end(args); 

    /* Prevent divide by 0 (unlikely) */
  if (count)
    return total / count;
  else
    return 0.0;
}
Notes:

On most 32-bit systems, this will work fine because integers and longs are the same size (4 bytes). On 64-bit systems, things get more complicated:

You can read all about the gory details here. It's definitely an advanced topic and beyond the scope of an introductory programming course. But, some may find it interesting/useful, especially if you want to explore the variadic nature of functions. It also makes you glad you're programming in a higher-level language than assembler!

Other Real World Uses

Suppose you have a variadic function that needs to pass all of the arguments to another function. You don't know what the types of the arguments are, nor do you care. You just need to forward them to a function that takes a variable number of arguments (like printf).

Here's an example of a program that wants to log all kinds of information about the state of the program. You may want to log integers, longs, strings, doubles, etc. Basically, any number and type of values. This sounds like a perfect case for a variadic function.

This is how the program may want to log information. (The sleep function just causes the program to pause for the specified number of seconds, simulating the time between log events.) Instead of just using printf to print the information, we want to put a time and date stamp on the output. In practice, you could add any kind of other information, as well.

#include <stdio.h>

  /* prototype for logging */
void logit(const char *fmt_string, ...);

int main(void)
{
    /* Arbitrary data */
  int i = 10;
  char c = 65;
  float f = 1.24F;
  double d = 3.14;
  char *s = "foobarbaz";

    /* Pretend to do stuff ... */
  logit("i is %i, c is %c, f is %f, d is %f, s is %s\n", i, c, f, d, s);
  sleep(1);
  logit("i is %i, c is %c, f is %f, d is %f, s is %s\n", i * 3, c + 7, f / 1.1, d / 1.2, s + 3);
  sleep(2);
  logit("i is %i, c is %c, f is %f, d is %f, s is %s\n", i + 8, c + 3, f * 3, d + 6, s + 6);
  sleep(1);
  logit("i is %i, c is %c, f is %f, d is %f, s is %s\n", i + 9, c + 2, f * 2, d + 5, s + 3);

  return 0;
}
The output might look like this:
Wednesday, February 23, 2022 12:14:33 PM PST: i is 10, c is A, f is 1.240000, d is 3.140000, s is foobarbaz
Wednesday, February 23, 2022 12:14:34 PM PST: i is 30, c is H, f is 1.127273, d is 2.616667, s is barbaz
Wednesday, February 23, 2022 12:14:36 PM PST: i is 18, c is D, f is 3.720000, d is 9.140000, s is baz
Wednesday, February 23, 2022 12:14:37 PM PST: i is 19, c is C, f is 2.480000, d is 8.140000, s is barbaz
Here's what the logit function might look like within the program:
#include <stdio.h>  /* printf, vprintf           */
#include <stdarg.h> /* va_list, va_start, va_end */
#include <time.h>   /* time, strftime, localtime */
#include <unistd.h> /* sleep (non-standard)      */

#define MAX_TIME_LEN 256

void logit(const char *fmt_string, ...)
{
  va_list args;           /* to access the arguments passed in  */
  struct tm *pt;          /* to convert the date/time           */
  time_t now;             /* to hold the current date/time      */
  char buf[MAX_TIME_LEN]; /* buffer to hold formatted date/time */

    /* Get the current system time (number of seconds since January 1, 1970) */
  now = time(NULL);

    /* Convert to local time */
  pt = localtime(&now);

    /* Format: Weekday, Month Day, Year HH:MM:SS AM/PM Timezone */
  strftime(buf, sizeof(buf), "%A, %B %d, %Y %I:%M:%S %p %Z", pt);

    /* Print formatted date/time */
  printf("%s: ", buf);

    /* Fetch the rest of the arguments and print them.
     * This is where the "magic" happens.
     */
  va_start(args, fmt_string);
  vprintf(fmt_string, args);

    /* Reset, required cleanup */
  va_end(args);
}

int main(void)
{
    /* Fake data */
  int i = 10;
  char c = 65;
  float f = 1.24F;
  double d = 3.14;
  char *s = "foobarbaz";

    /* Pretend to do stuff ... */
  logit("i is %i, c is %c, f is %f, d is %f, s is %s\n", i, c, f, d, s);
  sleep(1);
  logit("i is %i, c is %c, f is %f, d is %f, s is %s\n", i * 3, c + 7, f / 1.1, d / 1.2, s + 3);
  sleep(2);
  logit("i is %i, c is %c, f is %f, d is %f, s is %s\n", i + 8, c + 3, f * 3, d + 6, s + 6);
  sleep(1);
  logit("i is %i, c is %c, f is %f, d is %f, s is %s\n", i + 9, c + 2, f * 2, d + 5, s + 3);

  return 0;
}
There are a family of functions for this purpose: References for vprintf, vsprintf, vfprintf, and vsnprintf.