DEV Community

Paul J. Lucas
Paul J. Lucas

Posted on • Edited on

Variadic Functions in C

#c

Introduction

C has always used functions that can take a varying number of arguments — variadic functions — printf() being the primary example. Originally, C had no way for you to implement your own variadic functions portably. When function prototypes were back-ported from C++ to C, it included syntax for declaring variadic functions, for example:

int sum_n( unsigned n, ... );
Enter fullscreen mode Exit fullscreen mode

says the function sum_n requires one unsigned argument followed by zero or more other arguments.

A Simple Example Using a Count

Here’s a sample implementation of sum_n() where we use the required parameter to specify how many arguments follow:

#include <stdarg.h>  /* for va_*() macros */

int sum_n( unsigned n, ... ) {
  va_list args;
  va_start( args, n );
  int sum = 0;
  while ( n-- > 0 )
    sum += va_arg( args, int );
  va_end( args );
  return sum;
}
Enter fullscreen mode Exit fullscreen mode

Then you can call it like:

int r = sum_n( 3, 1, 2, 5 );  // r = 8
Enter fullscreen mode Exit fullscreen mode

Note that we are using the required parameter to specify how many arguments follow as our own convention here. The compiler does not infer any meaning from the required parameter. As we’ll see in a later example, the required parameter can be of any type.

Variadic Function Recipe

Any variadic function must be of the form:

R f( T1 p1, T2 p2, TN pN, ... ) {
  // ...
  va_list args;
  va_start( args, pN );

  // ... va_arg( args, T ) ...

  va_end( args );
  // ...
}
Enter fullscreen mode Exit fullscreen mode

that is you must:

  1. Declare a local variable of type va_list. (You can name it anything you want, but args is conventional.)
  2. Call va_start( args, pN ) where pN is the name of the last required parameter. Note that it can be of any type.
  3. To iterate over the values of the variadic arguments, call va_arg( args, T ) for each argument where T is its presumed type. (The type T may be different for each call.)
  4. Call va_end( args ) before returning.

Another Simple Example Using a Sentinel

Here’s a sample implementation of str_is_any() where the required parameter is a string to compare (the “needle”) and the arguments that follow are strings to compare against (the “haystack”). The arguments are terminated by a NULL pointer:

_Bool str_is_any( char const *needle, ... ) {
  va_list args;
  va_start( args, needle );
  _Bool found = false;
  do {
    char const *const hay = va_arg( args, char* );
    if ( hay == NULL )
      break;
    found = strcmp( needle, hay ) == 0;
  } while ( !found );
  va_end( args );
  return found;
}
Enter fullscreen mode Exit fullscreen mode

And you can call it like:

if ( str_is_any( type_str, "struct", "union", NULL ) )
Enter fullscreen mode Exit fullscreen mode

Caveats

Variadic arguments have several serious caveats:

  1. There is no way to require that any argument be of a specific type nor is there any way to require that all the arguments be of the same type.
  2. There is no way to know for certain what the type of any argument actually is.
  3. Because there is no type information, only default argument conversions occur (see below).
  4. There is no way to know how many arguments were given. (Attempting to access more arguments than were given results in undefined behavior; however, accessing fewer is OK.)
  5. Prior to C23, variadic functions had to have at least one required parameter.
  6. The ... must always be last.
  7. When iterating over arguments via va_arg(), the given type must match the actual type. If it doesn’t, the result is undefined behavior.

The default argument conversions are:

  • char, signed char, unsigned char, short, and unsigned short are promoted to either int or unsigned int as appropriate.
  • float is promoted to double.
  • An array is converted to a pointer to its zeroth element.
  • A function name is converted to a pointer to that function.

Hence, the top two problems when implementing a variadic function are:

  1. Knowing either the number of arguments or when to stop iterating over them.
  2. Knowing their types.

The sum_n() implementation “solves” the first problem by using the required parameter to specify how many arguments follow. However, if you were to do:

int r = sum( 3, 1, 2 );       // said 3, but only 2
Enter fullscreen mode Exit fullscreen mode

that is specify that there are 3 arguments that follow but there are fewer, the result would be undefined behavior.

Also, the sum_n() implementation can only assume that the provided arguments are of type int. If you were to do:

int r = sum( 3, 1, 2.7, 5 );  // double, not int
Enter fullscreen mode Exit fullscreen mode

that is provide a value of type double (or any other type) where int is expected, the result would be undefined behavior.

The str_is_any() function “solves” the first problem by using a sentinel so it doesn’t care how many arguments there are. However, it still can only assume that the provided arguments are strings and that the last argument is NULL. If either of those are false, the result would be undefined behavior.

The standard printf() function “solves” both problems by using the one required argument as the format for what to print: each % within the format is a conversion specifier and has a one-to-one correspondence with an argument. For example, given:

printf( "x=%d, y=%d\n", x, y );
Enter fullscreen mode Exit fullscreen mode

the printf() implementation scans the format string looking for % characters. Upon encountering one, it fetches the next variadic argument’s value via va_arg() using the type specified by the character(s) that follow the %, e.g., %d specifies int (and print it in decimal).

However, just as with sum_n(), if you either provide fewer arguments than specifiers or the type of a specifier and its associated argument don’t match, the result would be — you guessed it — undefined behavior.

Fortunately, modern compilers have specific knowledge about printf() (see “format” here), and so can warn when either the number of types of arguments don’t match the format string. For your own functions, however, you’re generally on your own to get it right.

Thoughts on Implementing Variadic Functions

Given all their caveats, are variadic functions a good idea? Not really. Their use was a hack stemming from C originally not caring about function arguments at all, so functions like printf() and scanf() took advantage of this. Even the introduction of stdarg.h (and varargs.h before that) did only the minimum amount to make implementing variadic functions portable, but not good.

Should you implement your own variadic functions? Generally, no. However, there is one use-case for implementing your own variadic functions.

Variadic Functions Calling Other Variadic Functions

In a large program that prints many messages, it would be helpful if you could know what line of code printed a given message so you can determine the state of the program at the time the message was printed.

For example, in a program like cdecl, if you get:

c++decl> explain int &*p
                     ^
13: error: pointer to reference is illegal; did you mean "*&"?
Enter fullscreen mode Exit fullscreen mode

you might want to know where in the source code that message was printed from. In many cases, you can just grep for the text of the message, but only if the message text appears literally in the code — which isn’t the case for this message.

I implemented a debug option for cdecl that, among other things, prints the source code location whence an error message came:

c++decl> set debug
c++decl> explain int &*p
                     ^
13: error: [c_ast_check.c:2170] pointer to reference is illegal; did you mean "*&"?
Enter fullscreen mode Exit fullscreen mode

The way this is implemented is that there’s an fl_print_error() variadic function that’s a wrapper around fprintf() that takes additional file and line arguments whence it was called. Here’s the (slightly simplified) implementation:

void fl_print_error( char const *file, int line,
                     char const *format, ... ) {
  fprintf( stderr, "error: " );
  if ( opt_cdecl_debug != CDECL_DEBUG_NO )
    fprintf( stderr, "[%s:%d] ", file, line );
  va_list args;
  va_start( args, format );
  vfprintf( stderr, format, args );
  va_end( args );
}
Enter fullscreen mode Exit fullscreen mode

and a macro that hides the passing of the file and line:

#define print_error(FILE,LINE,FORMAT,...) \
  fl_print_error( __FILE__, __LINE__, (FORMAT), __VA_ARGS__ )
Enter fullscreen mode Exit fullscreen mode

If you weren’t aware, printf() and fprintf() have vprintf() and vfprintf() counterparts that take a va_list parameter:

int vprintf( const char *format, va_list vlist );
int vfprintf( FILE *stream, const char *format, va_list vlist );
Enter fullscreen mode Exit fullscreen mode

A va_list parameter allows one variadic function to pass its variable arguments to another.

A Note on C23

As mentioned, as of C23, variadic functions no longer insist on at least one required parameter; that is you can do:

void f( ... ) {      // no required parameter
  va_list args;
  va_start( args );  // no second argument
  // ...
Enter fullscreen mode Exit fullscreen mode

This ability was also back-ported from C++.

Conclusion

Variadic functions in C are basically a hack. Given their serious caveats, you generally should not implement your own unless it’s a wrapper around another variadic function.

C++ inherited variadic functions from C, warts and all. However, in C++ there are the much better alternatives of function overloading, initializer lists, and variadic templates that can be used to implement functions that accept varying numbers of arguments in a type-safe way — but that’s a story for another time.

Top comments (1)

Collapse
 
callmemehdy profile image
MEHDI EL AKARY

those information were very helpful for me, thanks a lot!