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

  1. Write a variadic function that returns the largest of a counted list of ints and test it with one, three and five arguments.
  2. Add a prefix parameter to log_message and forward it to vprintf.
  3. 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.

  1. Which type must va_arg be asked for when the caller passed a float?

    Answer

    double, because of the default promotion

    A float argument is promoted to double in a variadic call, and va_arg must use the promoted type.

  2. 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.

  3. Why must a wrapper forward a va_list to vprintf?

    Answer

    Because ... cannot be passed on directly

    The v family accepts a va_list, so a wrapper starts the list and hands it to vprintf.

  4. 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.

  5. 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.

  6. 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.

  7. What does the logging wrapper print for value %d at %s?

    Answer

    [log] value 7 at noon

    The prefix is written first, then vprintf formats the message with 7 and noon.

  8. What does format_line print for the id and 42 call?

    Answer

    [id-42] 5

    The formatted text has five characters, and the return value reports that length.

  9. What happens when va_arg is asked for int but the caller passed a double?

    Answer

    The read is undefined and returns garbage

    va_arg does not convert; it reads the bytes as the named type, and the mismatch is undefined behaviour.

  10. 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 printf checks 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