58 Variadic functions
What to know first
Looking back
Chapter 29 taught that values crossing into variadic arguments undergo default promotions (float→double, small integers→int). But why is there a special promotion rule in that place alone?
A. Because the receiving side does not know the types. An ordinary function has its argument types written in its prototype (chapter 24) and the compiler makes the two sides match. But the variadic position has only ... in the prototype, so there is nothing to match against — hence the agreement “let us at least send everything unified into larger types” to reduce the confusion. This chapter is the story of how values are taken out at that “place that does not know types”, and what it costs.
The need for this chapter, and its context
printf has been in use since chapter 22, yet its secret opens only here. The deferral needed two things in place: the default argument promotions (chapter 29) and pointers (chapter 35). And its sitting just before chapter 59 is of a piece: variadic functions and function pointers read together as the two places where type checking goes slack.By the end of this chapter
printf could take any number of arguments. The four tools of <stdarg.h>, why this device is not type-safe, its history (from K&R’s varargs through C89′s stdarg to C23′s tidying), and today’s alternatives. Why chapter 22′s “the format is a contract” was such a strict contract is revealed here.The questions this chapter answers
- So has
_Genericgiven C generics? - How do I make my own function that wraps a formatting function like
printf— a logging function, say?
58.1 The four tools
What <stdarg.h> gives is one type and three macros — va_list (the reader), va_start (begin), va_arg (take one out), va_end (close). Seen in a demonstration.
examples-en/ch58/va.c
#include <stdarg.h>
#include <stdio.h>
/* taking the count as the first argument — it has no way of knowing the count itself */
int sum_n(int count, ...)
{
va_list ap;
int total = 0;
va_start(ap, count); /* read from after count */
for (int i = 0; i < count; i += 1) {
total += va_arg(ap, int); /* *we* are the ones telling it the type */
}
va_end(ap); /* it must be closed */
return total;
}
/* using a sentinel in place of a count */
int sum_until_zero(int first, ...)
{
va_list ap;
int total = first;
va_start(ap, first);
for (;;) {
int v = va_arg(ap, int);
if (v == 0) { break; }
total += v;
}
va_end(ap);
return total;
}
int main(void)
{
printf("sum_n(4, 1,2,3,4) = %d\n", sum_n(4, 1, 2, 3, 4));
printf("sum_until_zero(1,2,3,0) = %d\n", sum_until_zero(1, 2, 3, 0));
return 0;
}
Output
sum_n(4, 1,2,3,4) = 10
sum_until_zero(1,2,3,0) = 6
The key is the second argument of va_arg(ap, int) — the programmer states the type of the value to take out. The function does not know what arrived, so somebody must say “what I take out now is an int”, and that somebody is the author who shares the agreement with the caller. Break the agreement — take out an int when a double really arrived — and it is outside the contract (chapter 52). It reads the wrong place in memory, so the value becomes rubbish or every subsequent extraction is thrown out of step.
And the function does not know how many arguments there are either. So a way to learn the count must be made into a calling convention, and the demonstration shows two practices — receiving the count as the first argument (sum_n), or agreeing on a marker value announcing the end (sum_until_zero). printf uses a third road: the format string is a specification announcing both count and types. That is the exact weight of chapter 22′s “the format is a contract” — break that contract and printf goes rummaging through memory taking out arguments that do not exist (chapter 22′s format string vulnerability was exactly this mechanism).
58.2 Default argument promotions — values crossing ... get fatter
There is one more reason va_arg asks for a type. An argument crossing ... does not go as its original type. The default argument promotion seen in chapter 29 really happens here.
| what you passed | what actually arrives | so |
|---|---|---|
char, signed char, unsigned char | int (★) | va_arg(ap, char) is wrong |
short, unsigned short | int (★) | take it out with va_arg(ap, int) |
bool | int | the same |
float | double | va_arg(ap, float) is wrong |
integers at least as wide as int | unchanged | — |
| pointers | unchanged | but a null constant needs a cast |
Table 59.1
The rule in one line — integers narrower than int undergo integer promotion, and float becomes double. That is why printf has no float-specific format (chapter 61), and why writing va_arg(ap, float) is a contract violation, taking out a type that never arrived.
One qualification attaches to the star in the table. The result of integer promotion is almost always int, but exactly it is “int if an int can represent every value of the original type, otherwise unsigned int” (chapter 29). On today’s mainstream machines an unsigned short also fits in an int and so becomes int, but on an implementation where short and int have the same width an unsigned short promotes to unsigned int. Take it out there with va_arg(ap, int) and the signedness goes out of step — the exact rule is that you must use the type actually promoted to.
The trap when passing a null pointer comes from here too. On an implementation where NULL is defined as the integer 0, an integer 0 rides into the ... position, and if the receiving side takes it out with va_arg(ap, char*) the widths are out of step. That is why C23′s nullptr (chapter 36) or an explicit cast ((char *)0) is used.
58.3 The fundamental limit — the function knows nothing
A variadic function knows neither the number nor the types of the arguments that arrived. The values are simply placed, with no information accompanying them to explain what they are. So every variadic function must obtain that information from outside, and there are only three ways in the end.
| method | example | how it breaks |
|---|---|---|
| a specification string tells it | printf("%d %s", ...) | format and arguments out of step is UB. if the format is a variable, no check is possible |
| count and types are taken as arguments | sum_n(3, a, b, c) | miscount and it takes out arguments that are not there |
| an end marker is agreed | execl(..., (char *)NULL) | omit the marker and it does not stop |
Table 59.2
All three share the property that they hold only if a human keeps the agreement, because the information for a compiler to check simply does not exist. That is why chapter 85 counts this among “the five bugs C has been shipping for fifty years.”
58.4 _Generic — catching the type at compile time
C11 added one tool for this problem. _Generic is the syntax that chooses one of several options at compile time according to an expression’s type.
#define TYPE_NAME(x) _Generic((x), \
int: "int", \
double: "double", \
const char *: "string", \
default: "other")Three things to watch. First, the criterion for choosing is the type, not the value. Second, branches not chosen are not even compiled — so code valid only for one type can sit beside code valid only for another. Third, giving a type not in the list is a compile error unless there is a default — and that property is the heart of the next section.
examples-en/ch58/generic.c
#include <stdio.h>
/* C11 _Generic: it picks one at compile time according to the *type* of the
expression. That the run-time cost is zero is the point. */
#define TYPE_NAME(x) _Generic((x), \
_Bool: "bool", \
char: "char", \
int: "int", \
unsigned: "unsigned", \
long: "long", \
double: "double", \
float: "float", \
const char *: "string", \
char *: "string", \
default: "other")
/* bundling the value with a type tag — the same idea as proven's PROVEN_ARG */
enum arg_kind { A_INT, A_DOUBLE, A_STR };
struct arg {
enum arg_kind kind;
union { long i; double d; const char *s; } v;
};
static struct arg arg_from_int(long v) { return (struct arg){ A_INT, { .i = v } }; }
static struct arg arg_from_double(double v) { return (struct arg){ A_DOUBLE, { .d = v } }; }
static struct arg arg_from_str(const char *v) { return (struct arg){ A_STR, { .s = v } }; }
#define ARG(x) _Generic((x), \
int: arg_from_int, \
long: arg_from_int, \
double: arg_from_double, \
float: arg_from_double, \
const char *: arg_from_str, \
char *: arg_from_str)(x)
/* the function *knows* the type of what it receives — it does not trust a format string */
static void print_args(const struct arg *a, size_t n)
{
for (size_t i = 0; i < n; i++) {
switch (a[i].kind) {
case A_INT: printf(" [%zu] int %ld\n", i, a[i].v.i); break;
case A_DOUBLE: printf(" [%zu] double %.3f\n", i, a[i].v.d); break;
case A_STR: printf(" [%zu] string %s\n", i, a[i].v.s); break;
}
}
}
/* an array literal carries the count along too — these are not variadic arguments */
#define PRINT(...) do { \
struct arg _args[] = { __VA_ARGS__ }; \
print_args(_args, sizeof _args / sizeof _args[0]); \
} while (0)
int main(void)
{
int n = 42;
double x = 3.14159;
const char *s = "hello";
char c = 'A';
bool b = true;
printf("TYPE_NAME: %s %s %s %s %s\n",
TYPE_NAME(n), TYPE_NAME(x), TYPE_NAME(s), TYPE_NAME(c), TYPE_NAME(b));
printf("typed arguments:\n");
PRINT(ARG(n), ARG(x), ARG(s));
return 0;
}
Output
TYPE_NAME: int double string char bool
typed arguments:
[0] int 42
[1] double 3.142
[2] string hello
The latter part of the example is this chapter’s conclusion. ARG(x) uses _Generic to learn the value’s type and makes a struct carrying the value together with a type tag. Putting those in an array to pass means the function learns the count too — variadic arguments are not used at all. With no format string there is no place for format and arguments to disagree.
58.5 proven’s PROVEN_ARG — the structure in the flesh
The proven_println("{}", PROVEN_ARG(x)) used in chapter 91 is exactly this structure. Taken apart, the real implementation is three pieces.
① A bundle carrying a type tag and a value. The library represents one argument like this — an enumerated value saying what kind it is, and a union holding the value itself (exactly chapter 48′s tagged union).
typedef enum {
PROVEN_ARG_NONE, PROVEN_ARG_I32, PROVEN_ARG_U32,
PROVEN_ARG_I64, PROVEN_ARG_U64, PROVEN_ARG_F64,
PROVEN_ARG_CSTR, PROVEN_ARG_STR_VIEW, PROVEN_ARG_PTR,
PROVEN_ARG_CHAR, PROVEN_ARG_BOOL, PROVEN_ARG_CUSTOM, /* ... */
} proven_arg_type_t;② One construction function per type. Small functions such as proven_arg_i32(int v) that take a value, attach the tag and return it. All are static inline, so there is no call cost.
③ A macro choosing among those functions with _Generic. Here is the real header.
#define PROVEN_ARG(x) _Generic((x), \
_Bool: proven_arg_bool, \
char: proven_arg_char, \
signed char: proven_arg_i32, \
unsigned char: proven_arg_u32, \
short: proven_arg_i32, \
unsigned short: proven_arg_u32, \
int: proven_arg_i32, \
unsigned int: proven_arg_u32, \
long: proven_arg_i64, \
unsigned long: proven_arg_u64, \
long long: proven_arg_i64, \
unsigned long long: proven_arg_u64, \
double: proven_arg_f64, \
float: proven_arg_f64, \
const char*: proven_arg_cstr, \
char*: proven_arg_cstr, \
void*: proven_arg_ptr, \
proven_u8str_view_t: proven_arg_str_view,\
proven_arg_t: proven_arg_identity \
)(x)The knack of reading it is the (x) on the last line. _Generic settles into one function name, and the (x) after it calls that function. That is, PROVEN_ARG(n) compiles to proven_arg_i32(n) if n is an int, and to proven_arg_f64(n) if it is a double.
A few details of the design stand out.
- Narrow integers are gathered towards the fat side —
shortandsigned charboth go toproven_arg_i32. The same direction as the previous section’s default argument promotions, but here it is the library, not the compiler, deciding explicitly. floatis gathered intodouble— for the same reason.charis kept separate — there is a separateproven_arg_char, sochar c = 'Z'prints as the letterZrather than the number 90. ButPROVEN_ARG('Z')still goes as 90, because'Z'is anintby C’s rule — the header records this limit honestly in a comment.proven_arg_titself is in the list — an identity branch, so that a value already made into a bundle passes through unchanged when wrapped again.- A type not in the list is a compile error — there is no
defaultbranch. Pass a struct by mistake and the build fails. The exact opposite choice fromprintf, which accepted anything and collapsed at run time.
The rest is simple. proven_println("...", PROVEN_ARG(a), PROVEN_ARG(b)) binds the bundles into an array literal and passes it, with the count, to the real function. Nothing passes through a variadic position, so there is no promotion, no guessing at counts, and no format-argument mismatch.
Q. So has _Generic given C generics?
A. No. _Generic is a device for choosing among functions that already exist; it does not create code per type. A function must be written by hand for each type and the list maintained by hand. So it is used for a finite, well-known set of types, as in the standard library’s <tgmath.h> (choosing maths functions by type) or this section’s PROVEN_ARG. When real generics are needed, C still stamps code out with macros (chapter 92′s container macros) or passes void * with a size (chapter 85′s qsort).
58.6 History — from varargs to stdarg
This device’s history is a miniature of C growing into a language of contracts.
Before the standard (the K&R days). In the C of the era without prototypes (chapter 12), effectively no function had its argument count checked — a function like printf was nothing special. The way of taking arguments out was not portable either: each person used tricks such as walking the stack from the address of the first argument, and those tricks broke when machines appeared that passed arguments in registers rather than on the stack. Hence the Unix family’s <varargs.h> — the first attempt at wrapping the tricks in macros to hide machine differences.
C89 — <stdarg.h>. As the standards committee introduced prototypes (chapters 10 and 24), variadic arguments got formal syntax too: write ... in the prototype and take values out with the va_* macros. The decisive difference from the old varargs is that at least one named argument is required (va_start uses it as the reference point) — which is why signatures with the format first, like printf(const char *fmt, ...), became standard.
C23 — tidying. The restriction requiring a reference argument was relaxed (va_start’s second argument became optional), so a variadic function with no named argument can be written, and the old varargs.h vanished entirely into history. Over half a century it was refined from “unportable trick” through “standard macros” to “the excess removed.”
A common misconception. “Variadic functions are convenient, so use them often”
Convenient though they look, variadic arguments in C are the place where type safety disappears. The compiler does not check the arguments in the... position and the receiving side has no way to guess the types. That the printf family is even as safe as it is comes from compilers specially reading and checking the format string (chapter 17′s warnings), not from any general rule of the language — a variadic function you write yourself has no such net. The practical advice is clear: do not make one unless it is truly necessary. The alternatives are usually better — if the count is fixed, just list the arguments; if there are many, take an array and a length (chapter 38′s practice); if the kinds vary, take an explicit list of values with chapter 48′s tagged union. The modern taste in C library design is “variadic only where a tool’s checking attaches, as in logging and formatting.”Q. How do I make my own function that wraps a formatting function like printf — a logging function, say?
A. The standard has prepared v-prefixed partners for exactly that place — vprintf, vfprintf, vsnprintf and so on, the versions that take a va_list directly. Your function receives ..., makes a reader with va_start, and hands that reader over whole. If you want the format-checking warning on your function too, the practice is to attach a compiler extension notation (gcc’s and clang’s format attribute) — the pattern seen several times in this book, of tools filling in the safety the language cannot give.
We know the identity of variadic arguments — it was how to pass several values to a function. The next chapter is its mirror image: how to handle a function itself as a value, the function pointer.