Variadic Functions in C: A Comprehensive Guide

Variadic Functions in C: A Comprehensive Guide

Variadic Functions: A Deep Dive into a Powerful C Feature

Introduction

Variadic functions are functions that may take a variable number of arguments and are declared with ellipses (...) in place of the last parameter.

In C programming, the function must contain at least one named parameter:

int right(int a, double b, ...);

This is the correct way of declaring a variadic function, there has to be at least a single argument that is definite before using the ellipses.

int wrong(...);

This is the wrong way of declaring the variadic function, you can't just declare the function with just ellipses as the parameter, it won't work.

We will talk more about the declaration in the later part of the article.

Variadic Function Argument Macros

One of the most important things that the variadic function runs with is the Argument Access Macros, which are all defined in the header file stdarg, thus to be able to make use of variadic functions, it will have to be included in our code:

#include <stdarg.h>

These Macros are four, with one being a data type, and the remaining three being the main Macros. These are the macros:

  • Data type: va_list

  • Macro: va_start

  • Macro: va_arg

  • Macro: va_end

I will be explaining every one of them in detail.

Data type: va_list

This is used to declare the argument pointer variable. It is used to declare the variable that will hold the variadic data that will be passed as an argument into the variadic function at the point of the function call.

This is how it is been used to declare the argument pointer:

va_list ap;

in this case, the ap is the argument pointer that is declared with the data type va_list.

Macro: va_start

This macro initializes the argument pointer variable, that is the variable that is declared using the va_list, which in our case we use ap (ap - which signifies argument pointer). It initializes the argument pointer ap to point to the first optional arguments of the current function. It is very important to note that this macro takes in two arguments, which are:

  • The argument pointer variable, which in our case we are using the variable ap.

  • The last required argument to the function. The last required argument is the argument that marks the end of all the variable number of arguments that will be entered in the variadic function.

This is how the va_start is declared:

va_start(va_list ap, last-argument-required);

or we can first declare our va_list before implementing it in the va_start:

va_start(ap, last-argument-required);

Macro: va_arg

This macro returns the value of the next optional argument and modifies the value of the argument pointer(which is declared by the va_list), which in our case is ap, to point to the subsequent argument. Thus, successive uses of va_arg return successive optional arguments.

The type of the value returned by the va_arg is the type specified in the call. And this type must be the self-promoting type that matches the type of the argument.

Self-promoting data types, are types that are automatically promoted to a larger data type when used as arguments in the va_arg macro in C. Below are the Self-promoting data types:

Data TypePromotion
charint
short intint
floatdouble
_Bool or boolint

So these are the Data Types that get promoted while using them with va_arg, so in place of char, int is used, in place of short int, int is used, in place of float, double is used and in place of _Bool or bool, int is used.

The va_arg macro takes in two arguments, the argument pointer and the self-promotion data type.

This is how to use the va_arg:

va_arg(va_list ap, self-promotion-data-type)

or

va_list ap;
va_arg(ap, self-promotion-data-type);

Macro: va_end

Although there have been some arguments as to whether this macro is necessary or not, it is still a very good practice to implement it whenever we are making use of a variadic function.

This macro ends the use of the argument pointer (defined by the va_list) which in our case is ap. It is used at the end of the variadic function, where there is no further need for the argument pointer.

This macro takes in just a single argument, which in this case is the argument pointer, ap. This is how it is used:

va_end(va_list ap);

or

va_list ap;
/**
* Your code here
*/
va_end(ap);

All these macros are what we will be using to work with the variadic functions.

How to Declare a Variadic Function

To declare a variadic function, we have to take note of two important things, which are:

  • At least a single known argument.

  • Lastly, the ellipses (...).

For one to declare a function as a variadic function, one will have to include one or more known parameters, which inclusive of the parameter is the last required, more like what will tell the function the last variadic argument, as the function itself have no way of automatically knowing that.

Then finally, the last parameter which will be passed to the function is the ellipses (...) which in this case will let the function know that we will be taking a variable number of arguments, more like an unknown number of arguments will be coming in.

Finally, let us declare a function that will be able to accept a variable number of arguments and return their sum, so since the function will be returning their sum, that means it will have a return type of int and also will have the name added.

int add(int num_arg, ...);

This is how to declare a variadic function, here it is taking a known argument, which I named num_arg, remember what we said about telling the function the number of variable arguments to be expected? So using num_arg to signify several arguments and then finally using the ellipses(...) to signify that a variable number of arguments are expected.

The known argument mustn't always be an integer, it can also be a character pointer, just like in the case of printf.

int _printf(const char *format, ...);

In this case, what will give the function an idea of when to stop printing is the character pointer, which in this case is the format.

How to Define a Variadic Function

To define a variadic function, we will have to make use of our data type as well as our macros. These are the step-by-step method employed to be able to define a variadic function:

  1. First, you will need to include the stdarg.h header file as contained in it are all the macros and the data type we will be needing and using.

     #include <stdarg.h>
    
  2. Next, you will need to define a variadic function, in our case, I will define a variadic function with the name var_func, having a return type of int.

     #include <stdarg.h>
    
     int var_func(int num_arg, ...)
     {
         /**
         * your code comes here
         */
     }
    
  3. Next, you will need to declare an argument pointer variable of type va_list. In our case, we will name the argument pointer variable ap, and this is to be done inside of the variadic function, which in our case will be inside the var_func.

     #include <stdarg.h>
    
     int var_func(int num_arg, ...)
     {
         va_list ap;
     }
    
  4. Next, you will need to initialize the argument pointer variable using the va_start. The argument pointer when initialized points to the first optional argument of all the variable arguments.

     #include <stdarg.h>
    
     int var_func(int num_arg, ...)
     {
         va_list ap;
    
         va_start(ap, num_arg);
     }
    
  5. Next, you will need to use the va_arg to be able to access the optional arguments. The first call to the va_arg gives the first optional argument, the second call gives the second optional argument and so on.

     #include <stdarg.h>
    
     int var_func(int num_arg, ...)
     {
         va_list ap;
    
         va_start(ap, num_arg);
    
         va_arg(ap, data-type);
     }
    

    To be able to access more than one or all the optional arguments, we will need to do successive calls to va_arg, which we can implement using a loop. In our case, we will be making use of a while loop:

     #include <stdarg.h>
    
     int var_func(int num_arg, ...)
     {
         int i = 0;
         va_list ap;
    
         va_start(ap, num_arg);
    
         while (i < num_arg)
         {
             va_arg(ap, data-type);
             i++;
         }
     }
    

    Note: You can stop calling at any point even when you have not called all the optional arguments, but returning more than the arguments supplied may return garbage values.

  6. Finally, you will have to indicate that you are done with the argument pointer variable by calling va_end

     #include <stdarg.h>
    
     int var_func(int num_arg, ...)
     {
         int i = 0;
         va_list ap;
    
         va_start(ap, num_arg);
    
         while (i < num_arg)
         {
             va_arg(ap, data-type);
             i++;
         }
         va_end(ap);
     }
    

Steps 3, 4 and 6 must be performed in the function that accepts the optional arguments, but you can pass the va_list variable as an argument to another function and perform all or part of step 5 there.

You can Have more than one argument pointer variable if you like. You can initialize each variable with va_start when you wish, and then fetch arguments with each argument pointer as you wish.

Examples of Variadic Function

Example 1: Write a function that returns all the parameters.

We will be using variadic functions to access all the optional arguments and print them out:

#include <stdio.h>
#include <stdarg.h>

int print_all(int num_arg, ...)
{
    int i = 0, num;
    va_list ap;

    va_start(ap, num_arg);

    while (i < num_arg)
    {
        num = va_arg(ap, int);
        printf("%d\n", num);
        i++;
    }
    va_end(ap);
}

Now to test our function, we will need to combine it with our main function, so we will be able to call our function.

int main(void)
{
    int num;

    print_all(5, 2, 4, 8, -5, -8);

    return (0);
}

This function will print out all 5 arguments. Remember that the first argument specifies the number of optional arguments. Here we have 5 arguments, and thus we have 5 as the first argument as that is the number of arguments.

Example 2: Write a function that returns the sum of all its parameters.

We will be writing a function that returns the sum of all its parameters, no matter the number of arguments entered, so here is the code:

  1. First of all, we will include the necessary header files:

     #include <stdio.h>
     #include <stdarg.h>
    
  2. Next, we will define a variadic function named add_all, declared i and initialize it with 0, and also declared sum. And then used the various macros to set up the function:

     #include <stdio.h>
     #include <stdarg.h>
    
     int add_all(int num_arg, ...)
     {
         int i = 0, sum;
         va_list ap;
    
         va_start(ap, num_arg);
     }
    
  3. Next, we will use a while loop, so that we can be able to access all the arguments as we access them, we keep adding the arguments and storing the sum in the variable called sum and then finally return the sum at the end of the while loop.

     #include <stdio.h>
     #include <stdarg.h>
    
     int add_all(int num_arg, ...)
     {
         int i = 0, sum;
         va_list ap;
    
         va_start(ap, num_arg);
    
         while (i < num_arg)
         {
             sum += va_arg(ap, int);
             i++;
         }
         return (num);
         va_end(ap);
     }
    

Example 3: Write a function that prints anything.

  • Prototype: void print_all(const char *format, ...);

  • where format is a list of types of arguments passed to the function

    c: char

    i: integer

    f: float

    s: char * (if the string is NULL, print (nil) instead

    any other char should be ignored

  1. We will start first by defining the function and defining it:

     #include <stdio.h>
     #include <stdarg.h>
    
     void print_all(const char *format, ...)
     {
         va_list ap;
    
         va_start(ap, format);
     }
    
  2. Next, we will have to check the format before printing what is expected to be printed out. Where the format c should print a character, i should print an integer, f should print a float and s should print a string.

  3. So we will first have to first check if the format is not a null terminating character (\0) and keep checking the format to be able to perform accordingly based on whether it is a c, i, f or s before printing out what is supposed to be printed out in place of the format.

     #include <stdio.h>
     #include <stdarg.h>
    
     void print_all(const char *format, ...)
     {
         va_list ap;
    
         va_start(ap, format);
    
         if (format == NULL)
             exit(1);
    
         while (*format != '\0')
         {
             if (*format == 'c')
             {
                 printf("%c", va_arg(ap, int));
             }
             else if (*format == 'i')
             {
                 printf("%i", va_arg(ap, int));
             }
             else if (*format == 'f')
             {
                 printf("%lf", va_arg(ap, double));
             }
             else if (*format == 's')
             {
                 char *str = va_arg(ap, char *);
                 if (str == NULL)
                 {
                     printf("(nil) ");
                 }
                 else
                 {
                     printf("%s ", str);
                 }
             }
             format++;
         }
         va_end(ap);
     }
    

    Here, we check for the format using a while loop, why we decided to use a loop because we will have to go through all the formats available in search of what is needed while (*format != '\0').

    Next, we use the conditional if and if-else statement to check for the c, i, f and s and once that format is reached, then we print the optional arguments using the va_arg.

  4. We will now include this main function to test our function:

     int main(void)
     {
         print_all("ceis", 'G', 1, "Gideon");
         return (0);
     }
    
  5. We will now combine the two so that we can compile and run it:

     #include <stdio.h>
     #include <stdarg.h>
     #include <stdlib.h>
    
     void print_all(const char *format, ...)
     {
         va_list ap;
    
         va_start(ap, format);
    
         if (format == NULL)
             exit(1);
    
         while (*format != '\0')
         {
             if (*format == 'c')
             {
                 printf("%c ", va_arg(ap, int));
             }
             else if (*format == 'i')
             {
                 int num = va_arg(ap, int);
                 printf("%d ", num);
             }
             else if (*format == 'f')
             {
                 printf("%lf ", va_arg(ap, double));
             }
             else if (*format == 's')
             {
                 char *str = va_arg(ap, char *);
                 if (str == NULL)
                 {
                     printf("(nil) ");
                 }
                 else
                 {
                     printf("%s ", str);
                 }
             }
             format++;
         }
         va_end(ap);
     }
    
     int main(void)
     {
         print_all("ceis", 'G', 1, "Gideon");
         return (0);
     }
    

    This is the output we get when the code comes across the character c, it then prints the corresponding character in that position in the variadic function, which is B in this case, when it checks for the next character, which is e, there is no definition for e, so it goes to the next one which is i, then it prints an integer, in this case 3 is printed and then finally, it checks for the next format, which in this case is s, which then prints a string, in this case stSchool.

    This last example we just did is an application of variadic functions that can lead us to how a printf( ) and scanf( ) function work, that is how they are built, and that is the major application of variadic functions, they are used in building these two robust functions.

Conclusion

We were able to explore variadic functions to the fullest, next, we will be looking at how to combine variadic functions with a function pointer, as that is one of the most powerful and effective ways of writing efficient code that can be scaled anytime.

Variadic Functions are very powerful, it is what is used to build the printf function, which is one of the most robust and used functions in C programming, it is also what is used to build a scanf function, which also is another robust and used function in C.

With this knowledge, next, we will be building our printf function so you can check the articles to see how that is been done using the variadic function.

Thank you for reading. You can connect with me on Twitter and LinkedIn.