Variadic functions
printf takes a format and a variable number of arguments. A function of your own can do the same with stdarg.h: it needs a way to know how many arguments arrived, and it needs to read each one with the exact type that was passed. C checks neither, so both are the programmer's contract.
A counted argument list
The simplest contract is a count in the first parameter. After va_start the list can be read with va_arg once per element, and va_end closes it. The type passed to va_arg must match the promoted type of the argument: an int stays an int, a float arrives as a double.
#include <stdarg.h>
#include <stdio.h>
static int sum(int count, ...)
{
va_list arguments;
int total = 0;
va_start(arguments, count);
for (int i = 0; i < count; i++) {
total += va_arg(arguments, int);
}
va_end(arguments);
return total;
}
int main(void)
{
printf("sum: %d\n", sum(3, 4, 5, 6));
printf("sum: %d\n", sum(5, 1, 2, 3, 4, 5));
return 0;
}
gcc -Wall -Wextra sum.c -o sum
./sum
sum: 15
sum: 15
The first call passes four values after the count and the second passes five; both totals happen to be 15. A count larger than the real list makes va_arg read whatever is next on the stack, which is undefined behaviour the compiler cannot see.
A sentinel instead of a count
When the values are known to be non-zero, a zero can mark the end and the loop stops on it. The sentinel is part of the contract too: the caller must pass it, or the loop reads past the arguments. The first parameter is read before va_start and then the rest follows.
#include <stdarg.h>
#include <stdio.h>
static int sum_until_zero(int first, ...)
{
va_list arguments;
int total = first;
int value = first;
va_start(arguments, first);
while (value != 0) {
value = va_arg(arguments, int);
total += value;
}
va_end(arguments);
return total;
}
int main(void)
{
printf("total: %d\n", sum_until_zero(3, 4, 5, 0));
printf("total: %d\n", sum_until_zero(10, 20, 0));
return 0;
}
gcc -Wall -Wextra sentinel.c -o sentinel
./sentinel
total: 12
total: 30
The zero itself is added and changes nothing, so the totals are the sums of the values before it. The first parameter is a real argument that the function can name, which is why every variadic function in C has at least one fixed parameter.
Forwarding a va_list
A wrapper around printf cannot pass its own ... to another variadic function directly. It must forward the va_list to a function that ends in v, such as vprintf. The wrapper below adds a prefix and a newline to every message.
#include <stdarg.h>
#include <stdio.h>
static void log_message(const char *format, ...)
{
va_list arguments;
fputs("[log] ", stdout);
va_start(arguments, format);
vprintf(format, arguments);
va_end(arguments);
putchar('\n');
}
int main(void)
{
log_message("started");
log_message("value %d at %s", 7, "noon");
return 0;
}
gcc -Wall -Wextra log.c -o log
./log
[log] started
[log] value 7 at noon
The prefix is written before the format, so it appears once at the start of the line even when the message is long. The v functions exist for every printing function: vfprintf, vsnprintf and the rest of the family.
Building a string
vsnprintf formats into a buffer instead of a stream, which is how a wrapper returns the text to its caller. The buffer size and the return value follow lesson 26: the function writes at most size - 1 characters and returns how many the whole result would need.
#include <stdarg.h>
#include <stdio.h>
static int format_line(char *buffer, size_t size, const char *format, ...)
{
va_list arguments;
int written;
va_start(arguments, format);
written = vsnprintf(buffer, size, format, arguments);
va_end(arguments);
return written;
}
int main(void)
{
char line[16];
int needed = format_line(line, sizeof line, "%s-%d", "id", 42);
printf("[%s] %d\n", line, needed);
return 0;
}
gcc -Wall -Wextra build.c -o build
./build
[id-42] 5
Five characters fit in sixteen bytes and the return value equals the length, so nothing was cut. The same function with a smaller buffer would return a larger number and a truncated line, exactly as the snprintf section of lesson 26 showed.
No format checking
The compiler checks printf because it knows the function. A wrapper with the same shape is just another function, so the call below compiles: the format promises an int and the argument is a string.
#include <stdarg.h>
#include <stdio.h>
static void report(const char *format, ...)
{
va_list arguments;
va_start(arguments, format);
vprintf(format, arguments);
va_end(arguments);
}
int main(void)
{
report("value %d\n", "text");
return 0;
}
The program would print a number built from the pointer's bits, or crash, depending on the platform. The fix is one declaration with the format attribute, which tells GCC to apply the printf rules to the wrapper:
#include <stdarg.h>
#include <stdio.h>
static void report(const char *format, ...)
__attribute__((format(printf, 1, 2)));
static void report(const char *format, ...)
{
va_list arguments;
va_start(arguments, format);
vprintf(format, arguments);
va_end(arguments);
}
int main(void)
{
report("value %d\n", "text");
return 0;
}
gcc -Wall -Wextra attribute.c -o attribute
attribute.c: In function 'main':
attribute.c:18:20: warning: format '%d' expects argument of type 'int', but argument 2 has type 'char *' [-Wformat=]
18 | report("value %d\n", "text");
| ~^ ~~~~~~
| | |
| | char *
| int
| %s
The attribute names the function as a printf-like formatter, with the format at position 1 and the variable arguments at position 2. Every serious C project puts it on its logging wrapper, because it restores the check that the language otherwise leaves out.
Exercises
- Write a variadic function that returns the largest of a counted list of ints and test it with one, three and five arguments.
- Add a prefix parameter to log_message and forward it to vprintf.
- Write format_alloc, which measures the result with a first vsnprintf call, allocates the exact size and formats again.
Questions
A new set of questions in a new order every time. Choose one answer for each, then check.
-
Which type must
va_argbe asked for when the caller passed afloat?Answer
double, because of the default promotionA
floatargument is promoted todoublein a variadic call, andva_argmust use the promoted type. -
How does a variadic function know where its arguments end?
Answer
From a count or a sentinel in the arguments
The function has a fixed parameter such as a count or a terminating zero, and the loop follows that contract.
-
Why must a wrapper forward a
va_listtovprintf?Answer
Because
...cannot be passed on directlyThe
vfamily accepts ava_list, so a wrapper starts the list and hands it tovprintf. -
What does
__attribute__((format(printf, 1, 2)))add to a wrapper?Answer
The compiler check of a printf-like format
The attribute names the format position and the first variadic argument, restoring the checks the wrapper would otherwise lose.
-
What does this program print?
#include <stdarg.h> #include <stdio.h> static int sum(int c,...){ va_list a; int t=0; va_start(a,c); for(int i=0;i<c;i++) t+=va_arg(a,int); va_end(a); return t; } int main(void){ printf("%d %d\n", sum(3,4,5,6), sum(5,1,2,3,4,5)); return 0; }Answer
15 15
The first call adds 4, 5 and 6; the second adds 1 through 5; both totals are 15.
-
What does this program print?
#include <stdarg.h> #include <stdio.h> static int sum_until_zero(int first,...){ va_list a; int t=first,v=first; va_start(a,first); while(v!=0){ v=va_arg(a,int); t+=v; } va_end(a); return t; } int main(void){ printf("%d %d\n", sum_until_zero(3,4,5,0), sum_until_zero(10,20,0)); return 0; }Answer
12 30
The values before the zero are added, and the zero itself changes nothing, giving 12 and 30.
-
What does the logging wrapper print for
value %d at %s?Answer
[log] value 7 at noon
The prefix is written first, then
vprintfformats the message with 7 andnoon. -
What does
format_lineprint for theidand 42 call?Answer
[id-42] 5
The formatted text has five characters, and the return value reports that length.
-
What happens when
va_argis asked forintbut the caller passed adouble?Answer
The read is undefined and returns garbage
va_argdoes not convert; it reads the bytes as the named type, and the mismatch is undefined behaviour. -
What happens to a wrapper without the format attribute when the format is wrong?
Answer
It compiles without a format warning
The compiler treats the wrapper as an ordinary function, so the
printfchecks are lost until the attribute is added.
The code on this page was run with gcc (Alpine 15.2.0) 15.2.0 on Alpine Linux 3.24.3 (x86_64) in a Docker container.
Article author: Arthur Isaev