Variadic functions
Modern languages (like Python or Javascript) have a nice feature: they allow a function to have a variable number of arguments and, also, to define a default value for those arguments that are not specified.
This allows a more concise style of programming where some argument is specified only if it has not the "obvious" value.
C allows functions with a variable number of arguments (called variadic functions) through the use of a set of macros defined in the stdarg.h
header: va_start()
, va_copy()
, va_arg()
and va_end()
to scan the list of arguments.
While they are better then nothing, I've never really liked them as they are difficult to use and have several drawbacks:
- You have to define a way to signal the end of parameters. For example,
printf()
scans the formatting string to know how many parameters to expect whileexecl()
requires you to passNULL
as last argument. - There's no type check on the parameters. This can be useful sometimes but not in general and lead to hard to find bugs.
- The default value must be specified in the logic of the function itself, not in its definition.
I believe I'm not the only one disliking stdarg.h
since you can find a lot of libraries whose functions require you to specify dummy arguments to say that you don't need to specify a value (or just want a defaut value).
For example, the standard: strtol(s, end, base)
asks you to specify NULL
as the second argument to signal that you're not interested in the end of the number. I would prefer to have:
-
mystrtol(s,&end,16)
to get the end of the number -
mystrtol(s,16)
to just convert the string -
mystrtol(s)
to convert the a base 10 string
Let's see if we can do it.
C also offers variadic macros, i.e. macros with a variable number of arguments. Using them there are a couple of ways to avoid using stdarg.h
and save our day. I'll present here the cleanest one I use with an example and, below, will show how this is achieved.
Example 1: Using strtol()
with default values.
#include <stdlib.h>
#include "vrg.h"
#define mystrtol(...) vrg(mystrtol_, __VA_ARGS__)
#define mystrtol_1(s) strtol(s,NULL,10)
#define mystrtol_2(s,b) strtol(s,NULL,b)
#define mystrtol_3(s,e,b) strtol(s,e,b)
Now you can just write mystrtol("321")
to convert a 10 based string to a long integer and mystrtol("321",16)
to consider the string as a hexadecimal number.
If you don't mind a little bit more complexity, you can use _Generic
to also allow mystrol("321",&endptr)
to convert the string as a 10 base number and put the end pointer in endptr
:
#include <stdlib.h>
#include <stdint.h>
#include "vrg.h"
#define mystrtol(...) vrg(mystrtol_, __VA_ARGS__)
#define mystrtol_1(s) strtol(s,NULL,10)
#define mystrtol_2(s,b) \
_Generic((b),int: strtol(s,NULL, (intptr_t)(b)), \
char **: strtol(s,(char **)(b), 10))
#define mystrtol_3(s,e,b) strtol(s,e,b)
Note that I had to use intptr_t
to avoid getting a warning about converting a pointer to an integer of smaller size.
Example 2: Let's define a greet()
function that takes two arguments (a name and a message) and prints a greetings message but allow for the second argument to be optional. If it is not specified, a default message is used.
#include <stdio.h>
#include "vrg.h"
char * msg_default = "How are you?";
#define greet(...) vrg(greet, __VA_ARGS__)
#define greet1(n) greetX(n, msg_default)
#define greet2(n, m) greetX(n, m)
void greetX(char *name, char *msg) {
printf("Hello %s. %s\n", name, msg);
}
int main(int argc, char *argv[]) {
greet("Alice", "How's your day?");
greet("Bob"); // too lazy to think of a greeting for Bob
}
The code above, will produce the output:
Hello Alice. How's your day?
Hello Bob. How are you?
What we did is:
- name the function somewhat different (
greetX()
) and write it as we would normally do, with no notion of being variadic. - define
greet()
as a variadic macro (rather than a function) - define a
greet1()
macro for when the greet() macro is called with only one argument. - define a
greet2()
macro for when the greet() macro is called with two arguments.
It works pretty well and it seems to me much easier to write (and understand) than using stdarg.h
. Not to mention that this specific case is impossible to cover with stdarg.h
since the behaviour of va_arg()
is undefined if the argument does not exist!
There is the limitation that you must have at least one argument but this is true also for when you use stdarg.h
. GCC has an extension that would allow handling 0 arguments but I prefer to stay within the C11 standard boundaries.
Under the hood
The trick is in the vrg.h
file included in the code above, where vrg
serves as a mnemonic for variadic arguments.
I put an extended version on Github, but this is the only portion of vrg.h that is really relevant here:
#define vrg_cnt(vrg1,vrg2,vrg3,vrg4,vrg5,vrg6,vrg7,vrg8,vrgN, ...) vrgN
#define vrg_argn(...) vrg_cnt(__VA_ARGS__, 8, 7, 6, 5, 4, 3, 2, 1, 0)
#define vrg_cat0(x,y) x ## y
#define vrg_cat(x,y) vrg_cat0(x,y)
#define vrg(vrg_f,...) vrg_cat(vrg_f, vrg_argn(__VA_ARGS__))(__VA_ARGS__)
The first two macros are a pretty common way to count the number of arguments passed to a C macro. It's limited to 8 arguments because, to me, having more than few arguments (and 8 is a lot of arguments) it's a code smell that may indicate a bigger problem in how the functions are structured. Simply say no to functions with too many arguments!
The other two macros are the standard way to create an identifier piecewise.
The last one puts all together and calls a different function (or macro) depending on the number of arguments.
Note that this macro is safe as each argument is evaluated only once.
I hope you'll find these few simple macros useful to write (and easily mantain) variadic functions that are also easy to use.
*** NOTE: the latest version github also handles macros with 0 arguments. It's more complicated than the one presented here but gives you more flexibility.
Top comments (5)
This is awesome, thank you for making this post!
I needed this to handle 0 arguments as well, so I made a small modification to this macro in order to make the first comma optional,
from:
#define vrg_argn(...) vrg_cnt(__VA_ARGS__, 8, 7, 6, 5, 4, 3, 2, 1, 0)
to:
#define vrg_argn(...) vrg_cnt(__VA_ARGS__ __VA_OPT__(,) 8, 7, 6, 5, 4, 3, 2, 1, 0)
I'm not a macro wizard so I'm unsure how good/bad this may be but it solved my problem so I'm leaving this comment in case anyone else comes along with my same requirement.
Thanks magikz.
That's a good solution and much simpler than the one currently used to handle zero arguments.
I'm not adopting it yet because VA_OPT is only available in C23 which is still to be promoted from its "draft" state to an official standard (I believe it's expected to happen very soon).
I know many compilers already support it, but I tried to use only C11 features to minimize incompatibilities between compilers.
Thanks for your suggestion!
I love how C doesn't have any "modern" features, but C devs are like screw it, we'll figure out a way ourselves
Neat!
I like the argument counting. It is quite a clever use of the variadic macro, effectively "pushing" the argument count value into range with the number of arguments. I've not seen this before, nor considered it.
I was never a fan of variadic functions either, and this gets me thinking of other creative ways to mimic the behaviour.
It's really astonishing way to using "default parameters" in c.
i love way you do it.