2

In section 7.19.6.1 paragraph 8 of the C99 standard:

c If no l length modifier is present, the int argument is converted to an unsigned char, and the resulting character is written.

In section 7.19.6.1 paragraph 9 of the C99 standard:

If any argument is not the correct type for the corresponding conversion specification, the behavior is undefined.

  • Does the fprintf function require an int argument?

For example, would passing an unsigned int result in undefined behavior:

unsigned int foo = 42;

fprintf(fp, "%c\n", foo); /* undefined behavior? */

This worries me since an implementation could have defined char as having the same behavior as unsigned char (section 6.2.5 paragraph 15).

For these cases integer promotion may dictate that the char to be promoted to unsigned int on some implementations. Thus leaving the following code to risk undefined behavior on those implementations:

char bar = 'B';

fprintf(fp, "%c\n", bar); /* possible undefined behavior? */
  • Are int variables and literal int constants the only safe way to pass a value to fprintf with the %c specifier?
Community
  • 1
  • 1
Vilhelm Gray
  • 11,516
  • 10
  • 61
  • 114

2 Answers2

5

%c conversion specification for fprintf requires an int argument. The value has to be of type int after the default argument promotions.

unsigned int foo = 42;
fprintf(fp, "%c\n", foo);

undefined behavior: foo has to be an int.

char bar = 'B';
fprintf(fp, "%c\n", bar);

not undefined behavior:bar is promoted (default argument promotions) to int as fprintf is a variadic function.

EDIT: to be fair, there are still some very rare implementations where it can be undefined behavior. For example, if char is an unsigned type with not all char values representable in an int (like in this implementation), the default argument promotion is done to unsigned int.

Community
  • 1
  • 1
ouah
  • 142,963
  • 15
  • 272
  • 331
  • I think the OP was specifically worried about `char` unsigned and as wide as `int`, in which case `bar` is promoted to `unsigned int`. – Pascal Cuoq Jul 26 '13 at 14:21
  • @ouah Wouldn't *default argument promotions* for a `char` fall in the `integer promotions are performed on each argument` category of **section 6.5.2.2** paragraph 6? – Vilhelm Gray Jul 26 '13 at 14:22
  • I would be curious to know what glyph such an implementation would print for `CHAR_MAX` :) – Jens Gustedt Jul 26 '13 at 14:29
  • @VilhelmGray yes, I'm referring to this paragraph for the default argument promotions. – ouah Jul 26 '13 at 14:30
  • I wonder if the more current C standards have addressed this potential UB. Though since this probably only occurs on rare cases, it may not warrant a change even if they knew about it. – Vilhelm Gray Jul 26 '13 at 14:33
  • 1
    @JensGustedt I'd think all implementations with `UCHAR_MAX > INT_MAX` are freestanding implementations and freestanding implementations are not required to include the standard IO library. So I would think `fprintf` may be just not available on these implementations. – ouah Jul 26 '13 at 14:37
3

Yes, printf with "%c" requires an int argument -- more or less.

If the argument is of a type narrower than int, then it will be promoted. In most cases, the promotion is to int, with well defined behavior. In the very rare case that plain char is unsigned and sizeof (int) == 1 (which implies CHAR_BIT >= 16), a char argument is promoted to unsigned int, which can cause undefined behavior.

A character constant is already of type int, so printf("%c", 'x') is well defined even on exotic systems. (Off-topic: In C++, character constants are of type char.)

This:

unsigned int foo = 42;
fprintf(fp, "%c\n", foo);

strictly speaking has undefined behavior. N1570 7.1.4p1 says:

If an argument to a function has ... a type (after promotion) not expected by a function with variable number of arguments, the behavior is undefined.

and the fprintf call clearly runs afoul of that. (Thanks to ouah for pointing that out.)

On the other hand, 6.2.5p6 says:

For each of the signed integer types, there is a corresponding (but different) unsigned integer type (designated with the keyword unsigned) that uses the same amount of storage (including sign information) and has the same alignment requirements.

and 6.2.5p9 says:

The range of nonnegative values of a signed integer type is a subrange of the corresponding unsigned integer type, and the representation of the same value in each type is the same.

with a footnote:

The same representation and alignment requirements are meant to imply interchangeability as arguments to functions, return values from functions, and members of unions.

The footnote says that function arguments of types int and unsigned int are interchangeable, as long as the value is within the representable range of both types. (For a typical 32-bit system, that means the value has to be in the range 0 to 231-1; int values from -231 to -1, and unsigned int values from 231 to 232-1, are outside the range of the other type, and are not interchangeable.)

But footnotes in the C standard are non-normative. They are generally intended to clarify requirements stated in the normative text, not to impose new requirements. But the normative text here merely states that corresponding signed and unsigned types have the same representation, which doesn't necessarily imply that they're passed the same way as function arguments. In principle, a compiler could ignore that footnote and, for example, pass int and unsigned int arguments in different registers, making fprintf(fp, "%c\n", foo); undefined.

But in practice, there's no reason for an implementation to play that kind of game, and you can rely on fprintf(fp, "%c\n", foo); to work as expected. I've never seen or heard of an implementation where it wouldn't work.

Personally, I prefer not to rely on that. If I were writing that code, I'd add an explicit conversion, via a cast, just so these questions don't arise in the first place:

unsigned int foo = 42;
fprintf(fp, "%c\n", (int)foo);

Or I'd make foo an int in the first place.

Keith Thompson
  • 254,901
  • 44
  • 429
  • 631
  • When you state that my example "actually has well-defined behavior", are you saying that it is well-defined for my specific value of `42`, while possibly undefined for values outside of the range representable by `int`? – Vilhelm Gray Jul 26 '13 at 15:41
  • 1
    @VilhelmGray: Yes. It's well defined (if we pretend the footnote is normative) if and only if the value is with the ranges of both `int` and `unsigned int`. – Keith Thompson Jul 26 '13 at 15:53
  • 1
    @KeithThompson C also says the size and alignment of an `unsigned int` and `int` are the same in *p6) "For each of the signed integer types, there is a corresponding (but different) unsigned integer type [...] that uses the same amount of storage (including sign information) and has the same alignment requirements"* Although there is probably no chance using `unsigned int` would fail, it is technically UB because it clearly violates 7.1.4p1 *"If an argument to a function has [...] a type (after promotion) not expected by a function with variable number of arguments, the behavior is undefined"* – ouah Jul 26 '13 at 15:58
  • @KeithThompson Regarding the excerpt: `the very rare case that plain char is signed`, do you mean "unsigned" rather than "signed?" – Vilhelm Gray Nov 20 '13 at 19:04
  • 1
    @VilhelmGray: Yes, fixed, thanks! (To anyone reading these comments: the rare case isn't that plain `char` is unsigned, it's that plain `char` is unsigned *and* `sizeof (int) == 1`.) – Keith Thompson Nov 20 '13 at 19:09
  • @KeithThompson Therefore, explicitly passing a `signed char` variable should not result in undefined behavior since it's range of values is a subset of `int`'s range of values? – Vilhelm Gray Nov 20 '13 at 19:14
  • 1
    @VilhelmGray: Yes, I believe that's correct. Passing a `char` variable will *almost* never result in undefined behavior. In fact, I've never heard of a *hosted* implementation with `CHAR_BIT > 8`, and only hosted implementations are required to implement `fprintf`. – Keith Thompson Nov 20 '13 at 19:16