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)


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?
void change_pointer(char *ptr)
{
  ptr = "Bye";
}

int main(void)
{
  char *p = "Hello";
  printf("p = %s\n", p);
  
  change_pointer(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. An example 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 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 pointer */
  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. We can 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);
Output:
sum1 = 26, sum2 = 57, sum3 = 156
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 pointer */
  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.


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 pointer to list */
  va_end(args); 
}


The va_ macros are defined in stdarg.h as:

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.

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 (not 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 pointer */
  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:

An 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:

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, long);
    else if (*types == 'f')
      next = va_arg(args, double);

    total += next;
    count++;
    types++;
  }

    /* Reset pointer */
  va_end(args); 

    /* Prevent divide by 0 */
  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 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)
{
    /* 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\n", i, c);
  sleep(1);
  logit("i is %i, c is %c, f is %f\n", i * 3, c + 7, f / 1.1);
  sleep(2);
  logit("i is %i, c is %c, f is %f, d is %f\n", i + 8, c + 3, f * 3, d + 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);

   return 0;
}
The output might look like this:
Tuesday, November 29, 2016 08:02:53 AM PST: i is 10, c is A
Tuesday, November 29, 2016 08:02:54 AM PST: i is 30, c is H, f is 1.127273
Tuesday, November 29, 2016 08:02:56 AM PST: i is 18, c is D, f is 3.720000, d is 9.140000
Tuesday, November 29, 2016 08:02:57 AM PST: i is 19, c is C, f is 2.480000, d is 8.140000, s is foobarbaz
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 */
  va_start (args, fmt_string);
  vprintf (fmt_string, args);
  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\n", i, c);
  sleep(1);
  logit("i is %i, c is %c, f is %f\n", i * 3, c + 7, f / 1.1);
  sleep(2);
  logit("i is %i, c is %c, f is %f, d is %f\n", i + 8, c + 3, f * 3, d + 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);

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