char type in va_arg

Now as I understand it, C wants to promote char type to int. Why does C want to do this?

Because that's what the standard says. If you pass an integral value with conversion rank smaller than that of int (e. g. char, bool or short) to a function taking a variable number of arguments, it will be converted to int. Presumably the reason for this has its roots in performance, where it was (and in fact, often it still is nowadays) better to pass values aligned to a machine word boundary.

Second, is the best solution to cast the int back to a char?

Yes, but you don't really need a cast even, an implicit conversion will do:

char ch = va_arg(ap, int);

Variadic functions are treated specially.

For a non-variadic function, the prototype (declaration) specifies the types of all the parameters. Parameters can be of any (non-array, non-function) type -- including types narrower than int.

For a variadic function, the compiler doesn't know the types of the parameters corresponding to the , .... For historical reasons, and to make the compiler's job easier, any corresponding arguments of types narrower than int are promoted to int or to unsigned int, and any arguments of type float are promoted to double. (This is why printf uses the same format specifiers for either float or double arguments.)

So a variadic function can't receive arguments of type char. You can call such a function with a char argument, but it will be promoted to int.

(In early versions of C, before prototypes were introduced, all functions behaved this way. Even C11 permits non-prototype declarations, in which narrow arguments are promoted to int, unsigned int, or double. But given the existence of prototypes, there's really no reason to write code that depends on such promotions -- except for the special case of variadic functions.)

Because of that, there's no point in having va_arg() accept char as the type argument.

But the language doesn't forbid such an invocation of va_arg(); in fact the section of the standard describing <stdarg.h> doesn't mention argument promotion. The rule is stated in the section on function calls, N1570 paragraph 7:

If the expression that denotes the called function has a type that does include a prototype, the arguments are implicitly converted, as if by assignment, to the types of the corresponding parameters, taking the type of each parameter to be the unqualified version of its declared type. The ellipsis notation in a function prototype declarator causes argument type conversion to stop after the last declared parameter. The default argument promotions are performed on trailing arguments.

And the description of the va_arg() macro,, says (emphasis added):

If there is no actual next argument, or if type is not compatible with the type of the actual next argument (as promoted according to the default argument promotions), the behavior is undefined, except for the following cases:

The "default argument promotions" convert narrow arguments to int, unsigned int, or double. (An argument of an unsigned integer type whose maximum value exceeds INT_MAX will be promoted to unsigned int. It's theoretically possible for char to behave this way, but only in a very unusual implementation.)

Second, is the best solution to cast the int back to a char?

No, not in this case. Casts are rarely necessary; in most cases, implicit conversions can do the same job. In this particular case:

const char c = va_arg(ap, char);
putc(c, fp);

the first argument to putc is already of type int, so this is better written as:

const int c = va_arg(ap, int);
putc(c, fp);

The int value is converted by putc to unsigned char and written to fp.