C / TYPES AND REPRESENTATION
Integer promotion and the usual arithmetic conversions
Work out the exact type and value of any C arithmetic expression by applying the integer promotions and then the usual arithmetic conversions.
What you will learn
- Predict an expression's type: promote narrow operands, then compare rank and signedness
- Explain why unsigned char and short arithmetic cannot wrap at 8 or 16 bits
- Mask or cast after ~ and << on narrow types to keep the promoted bits out
- Pass int and double to variadic functions and read them back with va_arg
Understanding Integer promotion and the usual arithmetic conversions
C has no arithmetic operator that works on a type narrower than int. Before a value of type char, signed char, unsigned char, short, unsigned short, _Bool or a bit-field takes part in arithmetic, the integer promotions convert it: to int if int can represent every value of the original type, otherwise to unsigned int. On the usual targets, where int is 32 bits, all of those become plain int, including the unsigned ones, because a 32-bit int holds every value from 0 to 65535. The narrow types are storage formats; the arithmetic is done in int and only returns to a narrow type when you assign or cast.
Once both operands are int or wider, the usual arithmetic conversions choose one common type for the operation, and that type is the type of the result. If either operand is floating, the other is converted to the wider floating type. Otherwise the compiler compares integer ranks (long long above long above int): the higher rank wins when both operands have the same signedness, while with mixed signedness the unsigned type wins if its rank is at least as high, and the signed type wins only if it can represent every value of the unsigned type. Getting that final type right is what tells you the correct printf specifier, whether overflow wraps or is undefined, and whether >> shifts in copies of a sign bit.
Three places bend the pattern, and they cause most of the surprises. The shift operators promote each operand separately and never combine them, so the result type is the promoted left operand and the right operand's type is irrelevant. Assignment and compound assignment convert in the opposite direction, turning the int result back into the left operand's type, which is why c += 1 on a char looks like 8-bit arithmetic even though the addition happened in int. In a variadic call such as printf, arguments past the last declared parameter get the default argument promotions, integer promotions plus float to double, which is why %c reads an int and %f reads a double.
placeholder
<stdio.h>
TYPE_OF(x)
int main(void)
{
unsigned char a = 200, b = 200;
short s = -3;
unsigned int u = 1;
printf("a: %s\n", TYPE_OF(a));
printf("+a: %s\n", TYPE_OF(+a));
printf("a * b: %s = %d\n", TYPE_OF(a * b), a * b);
printf("a << 4: %s = %d\n", TYPE_OF(a << 4), a << 4);
printf("s / 2: %s = %d\n", TYPE_OF(s / 2), s / 2);
printf("s + u: %s\n", TYPE_OF(s + u));
printf("a + 1L: %s\n", TYPE_OF(a + 1L));
printf("a / 2.0: %s\n", TYPE_OF(a / 2.0));
return 0;
}
Arithmetic in C never happens in a type narrower than int: each operand is promoted first, then both are converted to a single common type that becomes the type of the result.
Worked examples
Bitwise NOT on a narrow type
Shows that ~ operates on the promoted int, so the complement covers 32 bits rather than 8.
<stdio.h>
int main(void)
{
unsigned char mask = 0x0F;
unsigned char inverted = ~mask;
printf("~mask as int: %d\n", ~mask);
printf("~mask as unsigned: %u\n", (unsigned)~mask);
printf("~mask & 0xFF: %d\n", ~mask & 0xFF);
printf("stored in unsigned char: %d\n", inverted);
return 0;
}
Example explained
Line 1~mask first promotes mask to int, so the operand is 0x0000000F and the result is 0xFFFFFFF0, which %d prints as -16.
Line 2Reading that same bit pattern as unsigned gives 4294967280, showing the complement touched all 32 bits, not just the low 8.
Line 3& 0xFF discards the bits that exist only because of the promotion, leaving the 8-bit complement 240.
Line 4Assigning to unsigned char converts -16 modulo 256, and printing that variable with %d works because it is promoted to int at the call.
Shifts promote only their left operand
Demonstrates that the result type of << is the promoted left operand and does not depend on the shift count's type.
<stdio.h>
int main(void)
{
unsigned char flags = 0x80;
unsigned long long shift = 1;
printf("sizeof flags = %zu\n", sizeof flags);
printf("sizeof (flags << shift) = %zu\n", sizeof (flags << shift));
printf("flags << shift = %d\n", flags << shift);
printf("(unsigned char)(flags << shift) = %d\n", (unsigned char)(flags << shift));
return 0;
}
Example explained
Line 1sizeof flags is 1, but sizeof (flags << shift) is 4: the shift is performed on the promoted int, not on the stored byte.
Line 2The unsigned long long count is promoted on its own and then ignored for typing purposes, so it cannot widen the result.
Line 3Because the result type is int, 0x80 << 1 keeps its ninth bit and prints 256 instead of dropping off the top of a byte.
Line 4The cast back to unsigned char is what finally removes that bit, which is why the last line prints 0.
Default argument promotions in a variadic call
Shows that char, short and float arguments arrive at a variadic function already widened to int and double.
<stdio.h>
<stdarg.h>
static int add_all(int count, ...)
{
va_list ap;
int total = 0;
va_start(ap, count);
for (int i = 0; i < count; i++)
total += va_arg(ap, int);
va_end(ap);
return total;
}
int main(void)
{
char c = 10;
short s = 20;
float f = 1.5f;
printf("add_all(2, c, s) = %d\n", add_all(2, c, s));
printf("%%f prints a promoted double: %.1f\n", f);
printf("%%c prints an int: %c\n", c + 55);
return 0;
}
Example explained
Line 1c and s are converted to int before the call, so va_arg(ap, int) is the only correct request; va_arg(ap, char) would be undefined behaviour.
Line 2f is promoted to double for the same reason, which is why %f is defined in terms of double and there is no separate float specifier.
Line 3c + 55 is already int arithmetic, and %c takes an int and converts it to unsigned char for output, printing A.
Important notes
Promotion looks at types, never at values: with short s = 1, the expression s * s is int arithmetic even though the result would fit in a short.
The code assumes 8-bit char, 16-bit short and 32-bit int and needs -std=c11 for _Generic, which reports a type without evaluating its operand; where int is 16 bits, unsigned short promotes to unsigned int instead.
Common mistakes
Expecting narrow unsigned arithmetic to wrap: with unsigned char x = 200, x * x is 40000 computed in int, not 64, so the modulo-256 value appears only after you assign or cast back.
Writing if (~flags == 0xFF) for unsigned char flags = 0: ~flags is the int -1, the comparison is false, and the intended byte mask needs (unsigned char)~flags.
Storing getchar()'s int result in a char before comparing with EOF: the comparison happens after promotion, so a genuine 0xFF byte compares equal to EOF where char is signed, and EOF never matches at all where char is unsigned.
Try it yourself
Change, predict, then run
Declare unsigned short a = 60000, b = 60000; print a + b with %d, then print (unsigned short)(a + b), and print sizeof (a + b) to show which type the addition actually used.
Open the C workspaceCheck your understanding
With unsigned short x = 50000, y = 3; on a platform where int is 32 bits and short is 16 bits, what is the type and value of x * y?
- int, 150000 — both operands are promoted to int because int represents every unsigned short value
- unsigned short, 18928 — the multiplication is done in the operands' own 16-bit type and wraps
- unsigned int, 150000 — one operand is unsigned, so the common type must be unsigned
- unsigned int, 18928 — the product wraps modulo 65536 and the result stays unsigned
Show answer
unsigned short has a rank below int, and a 32-bit int can hold 0 to 65535, so the integer promotions turn both operands into int; the usual arithmetic conversions then have nothing left to do because both are already int, and 150000 fits comfortably in int. Option 3 is tempting because unsignedness usually dominates in mixed expressions, but that rule is applied only after promotion, and neither promoted operand is unsigned any more; it would be correct only where int is 16 bits, in which case unsigned short promotes to unsigned int.