C++ / FUNDAMENTAL TYPES AND VARIABLES
Integer promotion and the usual conversions
After this you can name the type and predict the value of any mixed-type arithmetic expression in C++ by applying promotion then the usual conversions.
What you will learn
- Predict when an operand widens to int, and why unsigned char 200 + 200 is 400
- Walk the usual-conversions ladder to name the common type of a mixed expression
- Recognise the int/unsigned compare where -1 turns into 4294967295
- Widen a computation with a typed literal or cast, not with the assignment target
Understanding Integer promotion and the usual conversions
C++ has no arithmetic on types narrower than int. Before any binary arithmetic or bitwise operator, before unary +, - and ~, and before each operand of a shift, an operand of type bool, char, signed char, unsigned char, short, unsigned short, an unscoped enum or a bit-field is converted to int if int can represent all of its values, and to unsigned int otherwise. That one rule is why two unsigned chars holding 200 add up to 400 instead of wrapping at 255: the addition is int addition, and a byte-sized result only reappears when you store it back into a byte. The mental model to keep is that the operators are implemented for int and wider only, so narrow values are unpacked into an int, computed there, and repacked by you.
Once both operands are int or wider, the compiler picks a single common type using the usual arithmetic conversions. If either operand is a floating type, the other one becomes the wider floating type. Otherwise the operands are ranked (int < long < long long, by rank rather than by actual bit count) and compared by signedness: with the same signedness the lower rank converts up; with mixed signedness the unsigned type wins if its rank is at least as high, the signed type wins if it can represent every value of the unsigned type, and if neither holds both become the unsigned counterpart of the signed type. The operator's result type is that common type, which is why s + 1L has type long and i - u has type unsigned int.
Two consequences trip people up. The conversions look only at the two operands of one operator and never at the variable the result is assigned to, so long long n = a * b; with int a and b still multiplies in int and can overflow before any widening happens. And when a signed operand converts to unsigned, a negative value is reinterpreted modulo 2^32 on a 32-bit unsigned type, which makes -1 < 1u evaluate to false; the compiler is obeying the standard, so the repair is to cast, to change the declared types, or to use std::cmp_less from C++20.
<iostream>
const char* name(int) { return "int"; }
const char* name(unsigned int) { return "unsigned int"; }
const char* name(long) { return "long"; }
const char* name(unsigned long) { return "unsigned long"; }
const char* name(double) { return "double"; }
int main()
{
short s = 30000;
unsigned char b = 200;
int i = -1;
unsigned int u = 1;
std::cout << "+s : " << name(+s) << " " << +s << '\n';
std::cout << "s * s : " << name(s * s) << " " << s * s << '\n';
std::cout << "b + b : " << name(b + b) << " " << b + b << '\n';
std::cout << "s + 1L : " << name(s + 1L) << " " << s + 1L << '\n';
std::cout << "i - u : " << name(i - u) << " " << i - u << '\n';
std::cout << "i + 0.5 : " << name(i + 0.5) << " " << i + 0.5 << '\n';
}
An operator first widens its operands, narrow types to int and then both to one common type, so the type of an expression is decided by its operands and never by where the result is stored.
Worked examples
Promotion breaks byte-sized bit tricks
Shows that bitwise operators on an unsigned char work on a promoted int, so bits appear outside the original byte.
<iostream>
int main()
{
unsigned char mask = 0x0F;
std::cout << "sizeof(mask) = " << sizeof mask << '\n';
std::cout << "sizeof(~mask) = " << sizeof(~mask) << '\n';
std::cout << "~mask = " << ~mask << '\n';
std::cout << "~mask & 0xFF = " << (~mask & 0xFF) << '\n';
std::cout << "mask << 8 = " << (mask << 8) << '\n';
}
Example explained
Line 1The two sizeof lines show the promotion directly: the object is 1 byte, but the expression ~mask has type int and so 4 bytes.
Line 2~mask complements the promoted value 15 across all 32 bits, giving int -16 rather than the 240 you would get from a one-byte complement.
Line 3& 0xFF throws away the bits that promotion exposed, which is how you get the byte result 240 back.
Line 4mask << 8 is also int arithmetic, so the shifted bits survive past bit 7 and the value is 3840 instead of 0.
A negative int meeting an unsigned int
Demonstrates the mixed-signedness branch of the usual conversions in a comparison and in subtraction.
<iostream>
int main()
{
int delta = -1;
unsigned int count = 3;
std::cout << std::boolalpha;
std::cout << "delta < count : " << (delta < count) << '\n';
std::cout << "delta < (int)count : " << (delta < static_cast<int>(count)) << '\n';
std::cout << "count + delta : " << count + delta << '\n';
std::cout << "count - 4 : " << count - 4 << '\n';
}
Example explained
Line 1int and unsigned int have equal rank, so delta converts to unsigned: -1 becomes 4294967295 and the comparison is false. GCC and Clang warn about this under -Wall.
Line 2Casting count to int makes both operands signed, so the comparison uses the mathematical values and gives true.
Line 3count + delta is unsigned arithmetic modulo 2^32, and 3 + 4294967295 wraps to 2, so a correct-looking answer hides the type change.
Line 4count - 4 shows the same conversion without the lucky wrap: the result is 4294967295, and because unsigned wraparound is well defined this is not undefined behaviour, just the wrong number.
Important notes
The rules are fixed but the printed numbers are not: everything here assumes 32-bit int and unsigned int. Rank also matters more than width, so on Windows, where long is 32 bits, 1L + 1u has type unsigned long, while on Linux and macOS, where long is 64 bits, the same expression has type long.
Shift is the exception to the second stage: a << b promotes each operand separately and forms no common type, so the result type is the promoted type of the left operand alone.
Common mistakes
Comparing a possibly negative int with an unsigned value, as in delta < count above: the int becomes a huge positive number, so guard conditions take the wrong branch and countdown loops either never run or never stop.
Expecting long long n = 1000 * 60 * 60 * 24 * 30; to be computed in long long. All operands are int, so the product overflows int (undefined behaviour, typically a negative value) before the wide target is ever involved; write 1000LL * 60 * ... instead.
Assuming ~b or b << 4 keeps an unsigned char one byte wide. The operand promotes to int, so the result carries extra high bits and must be masked with & 0xFF before it is stored or printed.
Try it yourself
Change, predict, then run
Declare unsigned short a = 60000, b = 60000; then print a + b, sizeof(a + b), and static_cast<unsigned short>(a + b). Explain from the promotion rule why the first two lines are 120000 and 4, and why the third is 54464.
Open the C++ workspaceCheck your understanding
With int a = 100000; int b = 100000; long long c = a * b; on a platform with 32-bit int, what actually happens?
- The multiplication is carried out in long long because that is the type of c, so c is 10000000000.
- The multiplication is carried out in int, overflows, and the wide destination cannot recover the lost value.
- a and b are promoted to long long because int is narrower than long long.
- The product is computed exactly and then truncated to 32 bits, which is well-defined wraparound.
Show answer
The usual arithmetic conversions consider only the operands of *, which are both int, so the product is computed in int; 10000000000 does not fit, and signed overflow is undefined behaviour (in practice you often see 1410065408). Option 3 is tempting because unsigned arithmetic really is modular, but that guarantee does not extend to signed types, and no truncation step is even specified here. Promotion never widens int to long long either, so option 2 is wrong; write static_cast<long long>(a) * b to choose the operand type yourself.