char type in va_arg
Solution 1
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);
Solution 2
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 . The rule is stated in the section on function calls, N1570 6.5.2.2 paragraph 7:<stdarg.h>
doesn't mention argument promotion
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, 7.16.1.1, 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:
[SNIP]
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
.
Aman
Updated on June 04, 2022Comments
-
Aman about 2 years
I have the following function which writes passed arguments to a binary file.
void writeFile(FILE *fp, const int numOfChars, ...) { va_list ap; va_start(ap, numOfChars); for(int i = 0; i < numOfChars; i++) { const char c = va_arg(ap, char); putc(c, fp); } va_end(ap); }
Upon compiling, I get the following warning from the compiler
warning: second argument to 'va_arg' is of promotable type 'char'; this va_arg has undefined behavior because arguments will be promoted to 'int' [- Wvarargs]
Now as I understand it, C wants to promote char type to int. Why does C want to do this? Second, is the best solution to cast the int back to a char?
-
Aman over 9 yearsThanks Keith. I keep learning new things about C every day. :)
-
mafso over 9 years"But the language doesn't forbid [...]" -- I'm lost here. It's explicitly undefined: 7.16.1.1 (
va_arg
) p2 "[I]f 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: [...]" -
Keith Thompson over 9 years@mafso: Undefined behavior doesn't mean it's forbidden.
va_arg(ap, char)
is neither a syntax error nor a constraint violation; a conforming compiler is not required to reject or diagnose it. -
Keith Thompson over 9 yearsNeither a cast or an implicit conversion is needed. See the end of my (updated) answer.
-
mafso over 9 yearsI agree (I'm not even sure if a compiler is allowed to reject a program with an (unreached)
va_arg(ap, char)
, GCC warns that the program will abort if the code is reached), I was confused about "doesn't mention argument promotion". Anyway, I consider this answer useful with or without that part, even if I find it slightly misleading. (I would read "forbid" as "forbid for strictly conforming code", but maybe that's just me.) -
The Paramagnetic Croissant over 9 years@KeithThompson What do you mean by that no implicit conversion is needed? Surely you can't write
char ch = va_arg(ap, char)
since it's undefined? If you want achar
out of the argument list, you have to passint
tova_arg
, is that not right? -
Keith Thompson over 9 yearsWhat I mean is that the correct code would be
int c = va_arg(ap, int); putc(c, fp);
. You could makec
achar
, but sinceputc()
takes anint
argument there's not much point in doing so. -
Keith Thompson over 9 yearsIt's not incorrect (though it could have some problems).
va_arg(ap, int)
returns a result of typeint
, which is converted fromint
tochar
by the initialization ofch
. If plainchar
is signed and the result exceedsCHAR_MAX
(typically 127), then the result of the conversion is implementation-defined, though it will almost always undergo the usual 2's-complement wraparound. Ifch
is then passed toputc
, that function itself will convert it tounsigned char
; the result of that conversion is well defined. -
Keith Thompson over 9 yearsBut since
va_arg()
can't yield achar
, andputc
takes anint
argument, you might as well useint
consistently. (If the caller ofwriteFile
happens to pass an argument that's outside the range ofunsigned char
, it will be converted byputc
, which should normally give you the expected behavior.) -
The Paramagnetic Croissant over 9 years@KeithThompson Actually I was just confused about the wording "the correct code" – I'm aware of the implementation-defined behavior, and I agree that not relying on it is better than relying on it.
-
Keith Thompson over 9 yearsYou're right, the word "correct" was an overstatement.
-
Keith Thompson about 5 years@mafso: You were probably confused about "doesn't mention argument promotion" because that was wrong. I've just updated it.