Variadic functions in C: a new idiom?
Posted by kernelbob on April 5, 2010
For a long time, C has let you write functions that take a variable number of arguments. But C leaves it up to each variadic function to figure out how many arguments it’s been passed, and up to each caller to pass the right arguments.
Here’s a cute hack to make variadic functions easier to write and a little harder to call incorrectly.
As a contrived example, let’s write a function that copies a tag and a sequence of floating-point arguments into a linked list allocated on the heap. The first attempt might look like this.
#include <stdarg.h>
typedef struct node node;
struct node {
double datum;
char tag;
node *next;
};
node *link_list_A(char tag, int count, ...)
{
node *head = NULL;
node **patch = &head;
int i;
va_list ap;
va_start(ap, count);
for (i = 0; i < count; i++) {
node *p = malloc(sizeof *p);
p->datum = va_arg(ap, double);
p->tag = tag;
p->next = NULL;
*patch = p;
patch = &p->next;
}
va_end(ap);
return head;
}
You’d call it like this.
node *p = link_list_A('A', 5, 1.0, 1.5, 2.0, 2.5, 3.0);
There’s just a little problem with that. What happens if you go back later and change the arguments? You have to remember to change the count. Also, you might miscount and tell it it has 16 arguments when it really has 17.
Another approach is to use an end marker instead of a count.
# define ENDARG (-1.0)
node *link_list_B(char tag, ...)
{
node *head = NULL, **patch = &head;
double datum;
va_list ap;
va_start(ap, tag);
while ((datum = va_arg(ap, double)) != ENDARG) {
node *p = malloc(sizeof *p);
p->tag = tag;
p->datum = datum;
p->next = NULL;
*patch = p;
patch = &p->next;
}
va_end(ap);
return head;
}
…
node *p = link_list_B('B', 1.0, 1.5, 2.0, 2.5, 3.0, ENDARG);
That’s better. But wouldn’t it be even nicer if we could eliminate ENDARG in the caller? It’s a big wart on the readability of the program.
C99 has variadic macros as well as variadic functions. By wrapping the function in a macro, the macro can supply the end marker instead of sprinkling it throughout the source code.
(Hey, C99 has it’s been ratified for eleven years. Don’t look so scandalized.)
#define link_list_C(tag, ...) \ (link_list_B(tag, __VA_ARGS__, ENDARG)) // link_list_B defined above
Now you call it like this.
node *p = link_list_C('C', 1.0, 1.5, 2.0, 2.5, 3.0);
It’s not a big deal, but it makes the code a little more readable. And it’s an idiom I haven’t seen before.
I use the idiom several places in my Scheme interpreter, schetoo. For example, Schetoo has a C macro, MAKE_LIST, which is analogous to Scheme’s list procedure.
Scheme:
(list 'a 'b (list 'c))
=> (a b (c))
C:
obj_t a = make_symbol_from_C_str(L"a");
obj_t b = make_symbol_from_C_str(L"b");
obj_t c = make_symbol_from_C_str(L"c");
MAKE_LIST(a, b, MAKE_LIST(c));
=> (a b (c))
It’s a pretty good analogue, I think.
The source for schetoo is on github.
Jens Gustedt said
Hi,
I think you could even have a macro and a function of the same name, if you like:
#define link_list_B(tag, ...) \ (link_list_B(tag, __VA_ARGS__, ENDARG)) // function link_list_BThe
link_list_Bwould not get expanded, since the preprocessor inhibits recursion automatically. You only have to ensure that the macro definition comes after the function.Jens
Mac said
You don’t need ENDARG in all cases, because there’s a way to count the number of arguments given to a variadic macro:
http://groups.google.com/group/comp.std.c/browse_thread/thread/77ee8c8f92e4a3fb/346fc464319b1ee5
scootergarrett said
But what if one of the numbers you mean to enter in is a (-1.0) then it stops when it gets to it, is there something else that could be used for ENDARG? I tried NULL but it errors on that
Invalid operands to binary != (have ‘double’ and ‘void *’)
Garrett
kernelbob said
Sure. You can use a different value. If you’re using floats, maybe FLOAT_MAX, defined in limits.h. I _said_ the example was contrived…
scootergarrett said
Ya I just used
#define ENDARG (4.94065645841e-324)
which I found to be the smallest double possible. And I use NULL if I’m pulling in strings. This article helped me a lot and forced me to start thinking about macros. So as longs as I’m not pulling (4.94065645841e-324) into a function I should be ok; and if I’m doing that I have bigger problems. Thank you a lot.