Start with the example code shown in the stdarg man page (man 3 stdarg
). Slightly modified for readability, and adding a trivial main()
:
#include <stdlib.h>
#include <stdarg.h>
#include <stdio.h>
void foo(char *fmt, ...)
{
va_list ap;
int d;
char c, *s;
va_start(ap, fmt);
while (*fmt) {
switch (*(fmt++)) {
case 's':
s = va_arg(ap, char *);
printf("string %s\n", s);
break;
case 'd':
d = va_arg(ap, int);
printf("int %d\n", d);
break;
case 'c':
/* need a cast here since va_arg only
takes fully promoted types */
c = (char) va_arg(ap, int);
printf("char %c\n", c);
break;
}
}
va_end(ap);
}
int main(void)
{
char *s1 = "First";
char *s2 = "Second";
int d = 42;
char c = '?';
foo("sdcs", s1, d, c, s2);
return EXIT_SUCCESS;
}
If you compile and run the above, it will output
string First
int 42
char ?
string Second
As liliscent and Jonathan Leffler commented to the question, the key point is that we need a way to describe the type of each variadic argument. (In C, type information is essentially discarded at compile time, so if we want to support multiple types for one argument, we must also pass its type explicitly: the type (of a variadic function argument) simply does not exist at run time anymore.)
Above, the first parameter, fmt
, is a string where each character describes one variadic argument (by describing its type). Thus, there are expected to be the same number of variadic arguments as there are s
, d
, or c
characters in the fmt
string.
The printf family of functions and the scanf family of functions both use %
to indicate a variadic argument, followed by the formatting details and type specification of that argument. Because of the quite complex formatting they support, the code implementing those is much more complicated than the above example, but the logic is very much the same.
In an update to the question, OP asked if the function can change the value of the variadic arguments -- or rather, the values pointed to by the variadic arguments, similar to how scanf() family of functions work.
Because parameters are passed by value, and va_arg()
yields the value of the parameter, and not a reference to the parameter, any modifications we make to the value itself locally (to s
, d
, or c
in the foo()
function example above) will not be visible to the caller. However, if we pass pointers to the values -- just like scanf()
functions do --, we can modify the values the pointers point to.
Consider a slightly modified version of the above foo()
function, zero()
:
void zero(char *fmt, ...)
{
va_list ap;
int *d;
char *c, **s;
va_start(ap, fmt);
while (*fmt) {
switch (*(fmt++)) {
case 's':
s = va_arg(ap, char **);
if (s)
*s = NULL;
break;
case 'd':
d = va_arg(ap, int *);
if (d)
*d = 0;
break;
case 'c':
/* pointers are fully promoted */
c = va_arg(ap, char *);
if (c)
*c = 0;
break;
}
}
va_end(ap);
}
Note the differences to foo()
, especially in the va_arg()
expressions. (I would also suggest renaming d
, c
, and s
to dptr
, cptr
, and sptr
, respectively, to help remind us humans reading the code that they are no longer the values themselves, but pointers to the values we wish to modify. I omitted this change to keep the function as similar to foo()
as possible, to keep it easy to compare the two functions.)
With this, we can do for example
int d = 5;
char *p = "z";
zero("ds", &d, &p);
and d
will be cleared to zero, and p
to be NULL
.
We are not limited to a single va_arg()
within each case, either. We can, for example, modify the above to take two parameters per formatting letter, with the first being a pointer to the parameter, and the second the value:
void let(char *fmt, ...)
{
va_list ap;
int *dptr, d;
char *cptr, c, **sptr, *s;
va_start(ap, fmt);
while (*fmt) {
switch (*(fmt++)) {
case 's':
sptr = va_arg(ap, char **);
s = va_arg(ap, char *);
if (sptr)
*sptr = s;
break;
case 'd':
dptr = va_arg(ap, int *);
d = va_arg(ap, int);
if (dptr)
*dptr = d;
break;
case 'c':
cptr = va_arg(ap, char *);
/* a 'char' type variadic argument
is promoted to 'int' in C: */
c = (char) va_arg(ap, int);
if (cptr)
*cptr = c;
break;
}
}
va_end(ap);
}
This last function you can use via e.g.
int a;
char *b;
let("ds", &a, 2, &b, "abc");
which has the same effect as a = 2; b = "abc";
. Note that we do not modify the data b
points to; we just set b
to point to a literal string abc
.
In C11 and later, there is a _Generic
keyword (see e.g. this answer here), that can be used in conjunction with preprocessor macros, to choose between expressions depending on the type(s) of the argument(s).
Because it does not exist in earlier versions of the standards, we now have to use for example sin()
, sinf()
, and sinl()
to return the sine of their argument, depending on whether the argument (and desired result) is a double
, float
, or a long double
. In C11, we can define
#define Sin(x) _Generic((x), \
long double: sinl, \
float: sinf, \
default: sin)(x)
so that we can just call Sin(x)
, with the compiler choosing the proper function variant: Sin(1.0f)
is equivalent to sinf(1.0f)
, and Sin(1.0)
is equivalent to sin(1.0)
, for example.
(Above, the _Generic()
expression evaluates to one of sinl
, sinf
, or sin
; the final (x)
makes the macro evaluate to a function call with the macro parameter x
as the function parameter.)
This is not a contradiction to the earlier section of this answer. Even when using the _Generic
keyword, the types are checked at compile time. It is basically just syntactic sugar on top of macro parameter type comparison checking, that helps writing type-specific code; in other words, a kind of a switch..case statement that acts on preprocessor macro parameter types, calling exactly one function in each case.
Furthermore, _Generic
does not really work with variadic functions. In particular, you cannot do the selection based on any variadic arguments to those functions.
However, the macros used can easily look like variadic functions. If you want to explore such generics a bit further, see e.g. this "answer" I wrote some time ago.