char type in va_arg

12,905

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 <stdarg.h> doesn't mention argument promotion. The rule is stated in the section on function calls, N1570 6.5.2.2 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, 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.

Share:
12,905
Aman
Author by

Aman

Updated on June 04, 2022

Comments

  • Aman
    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
    Aman over 9 years
    Thanks Keith. I keep learning new things about C every day. :)
  • mafso
    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
    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
    Keith Thompson over 9 years
    Neither a cast or an implicit conversion is needed. See the end of my (updated) answer.
  • mafso
    mafso over 9 years
    I 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
    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 a char out of the argument list, you have to pass int to va_arg, is that not right?
  • Keith Thompson
    Keith Thompson over 9 years
    What I mean is that the correct code would be int c = va_arg(ap, int); putc(c, fp);. You could make c a char, but since putc() takes an int argument there's not much point in doing so.
  • Keith Thompson
    Keith Thompson over 9 years
    It's not incorrect (though it could have some problems). va_arg(ap, int) returns a result of type int, which is converted from int to char by the initialization of ch. If plain char is signed and the result exceeds CHAR_MAX (typically 127), then the result of the conversion is implementation-defined, though it will almost always undergo the usual 2's-complement wraparound. If ch is then passed to putc, that function itself will convert it to unsigned char; the result of that conversion is well defined.
  • Keith Thompson
    Keith Thompson over 9 years
    But since va_arg() can't yield a char, and putc takes an int argument, you might as well use int consistently. (If the caller of writeFile happens to pass an argument that's outside the range of unsigned char, it will be converted by putc, which should normally give you the expected behavior.)
  • The Paramagnetic Croissant
    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
    Keith Thompson over 9 years
    You're right, the word "correct" was an overstatement.
  • Keith Thompson
    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.