[...]
int
top_level(char a , ...)
{
va_list list;
va_start(list, a);
switch(a) {
case 1:
func_one(a, va_arg(list, char));
No problem here, assuming there is in fact a next `...'
parameter and its type is `char'. func_one() receives the
value of `a' and whatever value va_arg() produces.
[...]
Oops! I got sidetracked by other issues in the post and its
code, and missed something rather basic. There *is* a problem
here, because the argument expressions corresponding to `...'
parameters are subject to the "default argument promotions." This
means that a `char'-valued argument expression will be converted
to an `int' (or to an `unsigned int' on some machines), so the
called function receives an `int' and not a `char'. You must tell
va_arg the "as received" type, not the type as it was prior to
promotion. (7.15.1.1p2 lists a few cases where a type clash is
permissible; this is not one of them.)
More generally, it's hazardous to use any promotable type as
an argument corresponding to `...' parameters because it's hard
to know how to retrieve it with va_arg. A `float' argument always
promotes to `double', but "narrow" integer types are trickier:
- `signed char' promotes to `int'. `char' and `unsigned char'
promote to `int' on most machines, `unsigned int' on a few.
- `short' promotes to `int'. `unsigned short' promotes to
`int' on many machines, `unsigned int' on a few ("more").
- `enum' promotes to either `int' or `unsigned int', depending
on the underlying integer type the compiler chooses. (The
named constants are always `int' and do not promote; I'm
talking about `enum'-valued expressions.)
- Some of the <stdint.h> types may promote to `int', some to
`unsigned int', and some will not promote at all but will
come across as themselves. Which behave each way will vary
from machine to machine.
- "Semi-opaque" types like `size_t', `ptrdiff_t', `time_t'
and so on are not usually subject to promotion, but might
promote to `int' or `unsigned int' on some systems.
With lots of #if testing it is sometimes possible to code
the right va_arg invocation, as in
#include <limits.h>
...
#if CHAR_MAX <= INT_MAX
// `char' promotes to `int' on this machine
char c = va_arg(list, int);
#else
// `char' promotes to `unsigned int' on this machine
char c = va_arg(list, unsigned int);
#endif
.... and similarly for the other flavors of `char' and for the
variations on `short'. If you're using <stdint.h> types, you
can use its xxxx_MAX macros the same way, adding a third branch
for the non-promotion case. Some of the semi-opaque types have
xxxx_MAX macros and can be tested this way, some lack them and
can't be. `enum' types have no xxxx_MAX you could test.
A related issue arises when passing pointer arguments to `...'
functions: You may need casts at the point of call to coerce the
argument pointer to the type the called function will use with
va_arg. In particular, if you want to pass NULL as an argument
you nearly always need to cast it, because NULL might expand to
an unadorned 0 which the compiler will treat like an `int' rather
than like a pointer. So,
char *concatenate(char *first, ... /* string pointers */ );
...
char *color = "green";
char *food = "ham";
...
char *wrong = concatenate("I do not like ",
color, " eggs and ", food, ".", NULL);
...
char *right = concatenate("I do not like ",
color, " eggs and ", food, ".", (char*)NULL);
The take-away: Don't use promotable or even potentially
promotable argument expressions for `...' parameters, because
all those #if's will clutter your code abominably (and give you
lots of extra opportunities to botch something). Sorry for
overlooking this the first time around.