It was a pointer arithmetic hack, later abstracted away into a more portable form in some version of Unix; even later, it was adapted into ANSI C.
In many languages (like Pascal for example), variadic functions, if they were included at all, had to be handled as special cases. B, which was the predecessor to C, did not have to, because B did not require functions to be declared in advance at all. Because the caller did not have the information how many arguments a function accepted, the language implementation had to accept function calls with any number of arguments. This trait was inherited by early C, in which function declarations were optional, and even if present, they did not have to state how many arguments the function accepts. Implicit function declarations have been removed in C99, but only recent standardisation efforts are set to remove declarations that do not specify the function signature fully.
Early implementations of Unix took advantage of this freedom in defining printf
, by having it read a variable number of arguments depending on the format string received. Walking the argument list was done by pointer arithmetic on parameter pointers. V6 Unix for the PDP-11 did so in C0, and likewise did its Interdata 7/32 port1:
printf(afmt, args)
char *afmt;
{
register char *fmt;
register int *argp, left, c, n;
/*
* argp is used to step along list of arguments, since
* the number of args is not known in advance.
*/
argp = &args;
for (fmt = afmt; c = *fmt; fmt++) {
/* ... */
if (prf1(c, *argp, left) >= 0)
argp++;
}
}
V7 Unix added sprintf
and fprintf
and switched the implementation to PDP-11 assembly. You will notice this version also added support for the long
data type, which was previously absent. (Older versions seem to have alternated between C and assembly implementations of printf
.)
At some point, the <varargs.h>
header was created. Though I can say with some confidence it came from Unix, I am having a hard time definitively establishing in which version it appeared first. If The Unix Heritage Society site is to be believed, <varargs.h>
was there as early as in V7 Unix, dating January 1979. The contents of the header are pretty trivial:
typedef char *va_list;
# define va_dcl int va_alist;
# define va_start(list) list = (char *) &va_alist
# define va_end(list)
# define va_arg(list,mode) ((mode *)(list += sizeof(mode)))[-1]
This is basically encapsulating the same hack as above in just a little more abstract interface. The user of the header was supposed to declare a dummy parameter named va_alist
, set a dummy type for it using the va_dcl
macro, and then iterate over the arguments using a va_list
variable:
sum(va_alist)
va_dcl
{
va_list ap;
int result;
int x;
va_start(ap);
result = 0;
while ((x = va_arg(ap, int)) != -1) {
result += x;
}
va_end(ap);
return result;
}
I would assume that the invention of <varargs.h>
was part of an effort to make Unix more portable to different architectures. Of particular note is the va_end
macro: it seems to have been added purely as a forward-compatibility measure. It expands to nothing at all in the V7 Unix version, but allows other compilers to choose a wildly different implementation strategy.2
The ANSI C committee adapted the <varargs.h>
header into a slightly different <stdarg.h>
header that was more compatible with new-style, prototype-based function declarations (and as such, with non-traditional calling conventions that required prototypes to be present) while doing away with dummy parameter declarations. The changes being that va_dcl
has been removed, while va_start
was modified to additionally require passing the name of the last non-variadic parameter. The latter allowed existing implementations to keep using the same pointer arithmetic trick to access variadic arguments as they did before. This is explicitly mentioned in the same rationale document (§4.8.1.1) that @another-dave linked in his answer:
The parmN
argument to va_start
is an aid to writing conforming ANSI C code for existing C implementations. Many implementations can use the second parameter within the structure of existing C language constructs to derive the address of the first variable argument. (Declaring parmN
to be of storage class register
would interfere with use of these constructs; hence the effect of such a declaration is undefined behavior. Other restrictions on the type of parmN
are imposed for the same reason.) New implementations may choose to use hidden machinery that ignores the second argument to va_start
, possibly even hiding a function call inside the macro.
Modern compilers indeed seem to do the latter: in GCC and Clang, va_start
is defined in terms of an opaque compiler intrinsic __builtin_va_start
, and although it still requires the last non-variadic parameter to be passed, it is only for the sake of being able to warn when it is not what it ought be.
0 Note the source uses =-
and =+
for the operators that are now spelt -=
and +=
.
1 Linking to the .a
file, which fortunately displays well enough. The link to the tarball from the TUHS page for this release is now dead; save it while you can.
2 Although in practice I am having a hard time finding an implementation of va_end
that does anything nontrivial. Other than the completely opaque definition in terms of __builtin_va_end
used by GCC and Clang, most expand to nothing at all, or to (void) 0
; some reset any pointers within the passed va_list
(or va_list
itself) to null. The most interesting version of va_end
is the one I found in the Acorn C/C++ manual (p. 103):
#define va_end(ap) ((void)(*(ap) = (char *)-256))
In principle, though, it would be valid to have an implementation that performs memory allocation in va_start
, or even opens a block, intending it to be matched by a counterpart in va_end
. The ANSI C design rationale document remarks on va_end
(§4.8.1.3):
In many implementations, this is a do-nothing operation; but those implementations that need it probably need it badly.
The ‘probably’ seems telling: it’s as if the committee wasn’t sure either whether anyone actually found va_end
useful.
int fn()
means a function taking any number of arguments.varargs.h
relied on.