Varargs are bad, m'kay.
are a facility in C for managing functions with a variable number of arguments. In the dim dark days of C, any function could be passed any number of arguments as long as that number was greater than or equal to the number of arguments declared -- if you were lucky (some compilers didn't verify). This is a very assembly language approach to function calls. Basically, the caller pushes all the args needed on the stack, calls the function and upon return, the caller consumes the args.
In Pascal, the callee consumes the arguments passed in. This is not so good because every Pascal function has to end with something like this:unlink a6 ; clean up the stack frame
move.l (sp)+, a0 ; grab the return address
lea n(sp), sp ; n is the number of bytes used by arguments
jmp a0 ; jump to the return address
Whereas in C, you get this:unlink a6;
And the caller has the lea instruction to clean up the stack. That adds 1 instruction of overhead to the call.
ANSI C allows stronger typing of arguments to a function by adding the token '...' to the language. A function declaration that takes a variable number of args looks like this:extern void MyFunc(long argCount, ...);
That's all well and good - now the compiler can check that MyFunc gets the minimum its needed to make it work.
There are at least two problems with varargs
- varargs are not necessarily compiler portable
- A function using varargs cannot correctly verify its arguments
I encountered the first case with a library from an outside vendor in which sizeof(int) == sizeof(long). The compiler I used to link with their library was set such that sizeof(int) == sizeof(short). If I called the function like this:MyFunc(2, 0, 0);
I was pushing a long and two shorts (4 bytes) onto the stack, whereas MyFunc() was expecting three longs (6 bytes), which means that the final two bytes were being taken from my code's local variables or from whatever else happened to be on the top of the stack at the time. If what was on the stack was a short-sized zero, everything was fine. If it sometimes was not, then MyFunc() would now behave erratically. This is one of, if not the worst kind of bug to track down since it is not always reproduceable. Fortunately, this type of compiler difference is not as common as it used to be.
The second case is far more common and arises when there is a mismatch between what the caller is supplying and what the callee is expecting. To try to avoid this, creators of varargs routines rely on conventions to manage this. For example, args sometimes get passed in as pairs - a type constant followed by the actual value ending with a sentinel:SomeFunc(kInt, 3, kDouble, 2.0, kString, "blah", kLastArg);
Other times a descriptor is passed in first which describes the arguments:printf("%d %f %s\n", 3, 2.0, "blah");
Other times, the first argument is a command:DispatchMessage(kPaintWindow, &bounds, blackColor);
No matter what, it is possible to break the callee by passing in something that doesn't follow the convention. The problem is that there is nothing that can be done to enforce the convention. Without enforcement, you get a crash if you're lucky. If you're not lucky, you get a time bomb set in your heap that will go off only if the conditions are right.
In the last example, you're about as safe as you can get as long as you don't actually called DispatchMessage(), but instead use author-supplied macros like:#define PaintWindow(bounds, color) DispatchMessage(kPaintWindow, bounds, color)
because then at least DispatchMessage is called correctly as long as the macro matches convention, and it is a single point of failure (which means a single point for a fix, which we like). I would turn around and argue that a single API, DispatchMessage, would be better off being multiple APIs.
Here's the skinny - if you're using varargs, you're probably doing something wrong out of the gate. Varargs promises a lot, but delivers very little.