Consider this code:

#include <limits> #include <iostream> int main() { // assume this static assert passes static_assert(sizeof(unsigned short) < sizeof(int)); unsigned short one = 1; unsigned short max = std::numeric_limits<unsigned short>::max(); unsigned short sum = one + max; if (sum == one + max) std::cout << "sum = one + max, and sum == one + max\n"; else std::cout << "sum = one + max, but sum != one + max\n"; return 0; }

*Figure 1*

When you run this program you’ll get the output

sum = one + max, but sum != one + max

Here’s a link to it on wandbox if you want to try it out. For clarity, there’s no undefined behavior in the program and the compiler isn’t doing anything wrong.

Surprising?

C and C++ both perform “integral promotion” when they encounter an operator that has an operand of integral type with lesser rank than type *int*. In practice, integral promotion means that any operand of integral type with smaller bit-width than type *int* will be implicitly converted (promoted) by the compiler to type *int*. Otherwise the integral operand type will remain unchanged.

In Figure 1, if the static_assert passes, the assignment

unsigned short sum = one + max;

will be translated by the compiler into

unsigned short sum = (unsigned short)((int)one + (int)max);

On a typical system today, the variable **max** (of unsigned short type) in Figure 1 would likely be assigned the value 65535, and would retain the same value when converted to type *int*. The variable **one** obviously contains the value 1, and will retain its value after being converted to type *int*. The addition of these two (converted/promoted) type *int* values will result in the type *int* value 65536. The compiler finally will cast the result from type *int* to type *unsigned short* in order to assign the result to variable **sum**. The value 65536 wouldn’t be representable in type *unsigned short* (for this hypothetical system), but the conversion is well-defined in C and C++; the conversion is performed modulo 2^N, where N is the bit width of type *unsigned short*. In this example, N=16 and thus the conversion of 65536 will result in the value 0, which is assigned to **sum**.

A similar process takes place for the line

if (sum == one + max)

except that there is never any narrowing conversion back to *unsigned short*. The equality operator has **sum** as its left hand side operand, so **sum** will be promoted to type *int*. **one** and **max** will be promoted to type *int,* since they are operands for the addition operator. And their summation is the type *int* value 65536. When evaluating the conditional, 65536 compares as unequal to the promoted value of **sum** (which equals zero), and so the program reports that “`sum = one + max, but sum != one + max".`

When **sum** was assigned, a narrowing conversion took place, but the right hand side of the conditional never needed any narrowing conversion at all. The hidden integral promotions and the hidden narrowing conversion are subtle, and the end result can be very surprising.

## Undefined Behavior For Promoted Unsigned Integral Types

Now that we’re a bit familiar with integral promotion, let’s look at a different example:

#include <limits> unsigned short foo(unsigned short x) { // assume this static assert passes static_assert(sizeof(unsigned short) < sizeof(int)); unsigned short max = std::numeric_limits<unsigned short>::max(); unsigned short result = max * x; if (x < max - 10) return 0; return result; }

*Figure 2*

Despite all the lines seeming to involve only type *unsigned short*, there is a potential for undefined behavior in Figure 2 due to possible signed integer overflow on type *int*. The compiler will implicitly perform integral promotion for the line

unsigned short result = max * x;

so that the multiplication will involve two (promoted/converted) operands of type *int*, not type *unsigned short*. If for our compiler *unsigned short* is 16 bit and *int* is 32 bit, then any product of **max** and **x** larger than 2^31 would overflow the signed type *int*, which is undefined behavior. Again assuming *unsigned short* is 16 bit and *int* is 32 bit, **max** will equal 65535, and so if the function parameter **x** is greater than 32768, overflow will occur. It doesn’t matter that overflow of unsigned integral types is well-defined behavior in C and C++. No multiplication of values of type *unsigned short *ever occurs in this function (assuming that the static_assert succeeds).

A perverse consequence of the integral promotion in Figure 2 is that a compiler would be within its rights to “optimize” the object code for Figure 2 (if the static_assert succeeds), and generate very fast and almost certainly unintended object code equivalent to

unsigned short foo(unsigned short x) { return 0; }

To understand why, let’s consider from Figure 2 the lines

unsigned short result = max * x; if (x < max - 10) return 0;

For all values of **x** that fail this conditional, the earlier multiplication product will have certainly overflowed the promoted signed *int* type. Modern C/C++ compilers (for better or worse) commonly optimize by taking advantage of the fact that undefined behavior is impossible in any valid code, and so a compiler may conceivably assume that any code that calls foo() will never call it with an argument that would result in undefined behavior. It therefore could assume the conditional in foo() will/must always succeed – since the alternative would be undefined behavior from the overflow, which is impossible. The result would be a function that does nothing but return 0. [If it’s any reassurance, I haven’t found a compiler that currently perform this optimization on Figure 2.]

Let’s look at one last example:

unsigned short bar(unsigned short x, unsigned short y) { // assume this static assert passes static_assert(sizeof(unsigned short) < sizeof(int)); unsigned short result = (x-y) << 1; if (x >= y) return 0; return result; }

*Figure 3*

The subtraction operator in Figure 3 has two unsigned short operands **x** and **y**, both of which will be promoted to type *int*. If **x** is less than **y** then the result of the subtraction will be a negative number, and left shifting a negative number is undefined behavior. Keep in mind that if the subtraction had involved unsigned integral types (as it would appear on the surface), the result would have underflowed in a well-defined manner and wrapped around to become a large positive number, and the left shift would have been well-defined. But since integral promotion occurs, the result is a negative number and the left shift is undefined behavior. For similar reasons as given for Figure 2, the compiler could potentially “optimize” the code of Figure 3 so that it does nothing but return 0.

## Integral Types That Can be Promoted

There’s implementation-defined behavior involved with integral promotion that’s worth discussing. It’s up to the compiler to decide the exact sizes for the types *char, unsigned char, signed char, short, **unsigned short*, *int, unsigned int, long, unsigned long, long long, *and* unsigned long long*. The only way to know if one of these types has a larger bit-width than another is to check the compiler’s documentation (or run a program that outputs the sizeof() result for the types). Thus it’s implementation defined whether *int *has a larger bit width than *unsigned short*, and by extension it’s implementation defined whether *unsigned int *will be promoted to type *int*. The standard does effectively guarantee that types *int, unsigned int, long, unsigned long, long long, *and *unsigned long long* will never be promoted. Floating point types of course are never subjected to integral promotion.

Dependent upon the implementation-defined type sizes, this still leaves far more integral types than you’d expect which might be promoted.

A non-exhaustive list of types that might be promoted is

*char, unsigned char, signed char, short, unsigned short, int8_t, uint8_t, int16_t, uint16_t, int32_t, uint32_t, int64_t, uint64_t, int128_t, uint128_t, int_fast8_t, uint_fast8_t, int_least8_t, uint_least8_t, int_fast16_t, uint_fast16_t, int_least16_t, uint_least16_t, int_fast32_t, uint_fast32_t, int_least32_t, uint_least32_t, int_fast64_t, uint_fast64_t, int_least64_t, uint_least64_t, int_fast128_t, uint_fast128_t, int_least128_t,* *uint_least128_t*

Ironically the sized integral types (int32_t, uint64_t, etc) are all open to potential integral promotion, dependent upon the implementation-defined size of *int*. It’s not unreasonable to think that there may be a compiler for special purpose hardware that defines *int* as a 64 bit type, and if so, *int32_t *and* uint32_t* will be subject to promotion to that larger *int* type. In theory, there’s nothing in the standard that would prevent a future compiler from defining *int* as a 128 bit or 256 bit type, and so we have to add *int64_t, uint64_t, int128_t,* and *uint128_t *as types that might be promoted, depending on how the compiler defines type *int*.

Very realistically in code today, *unsigned char, unsigned short, uint8_t *and *uint16_t *(and also *uint_least8_t, uint_least16_t, uint_fast8_t, **uint_fast16_t*) should be considered a minefield for programmers and maintainers. On most compilers (defining *int* as at least 32 bit), these types don’t behave as expected. They will usually be promoted to type *int* during operations and comparisons, and so they will be vulnerable to all the undefined behavior of the signed type *int, *and unprotected by any well-defined behavior of the original unsigned type, which does not apply – despite appearances.

## History

It seems like it would have been preferable if the C and C++ standards had mandated that unsigned integral types be promoted to type *unsigned int*. It would have prevented the very surprising undefined behavior that can be introduced by the current promotion/conversion of these types to signed *int*.

I’m certainly not the first person to discuss their thoughts about how unsigned integral promotion ideally ought to work. The issues were very well known to the first ANSI C standard committee in the late 80s. In fact, they recognized that the approach to unsigned promotion that I like would have had some very confusing and surprising consequences for programmers. Consider this code:

int foo() { // assume this static assert passes static_assert(sizeof(unsigned short) < sizeof(int)); unsigned short x = 42; if (x > -1) return 1; else return 0; }

*Figure 4*

Everything works as most programmers would expect, using the current integral promotion rules that the committee put into place. Foo() will return 1. But if instead the committee had adopted the hypothetical rule of promoting *unsigned short* to *unsigned int* – then very surprisingly to most programmers, foo() would have returned 0. Under a rule of promoting unsigned types to *unsigned int*, **x** would be promoted to *unsigned int* before doing the comparison, and as a result the constant -1 (which has type *int*) would need to be converted to type *unsigned int *(as mandated by the standard elsewhere) in order to be compared to the promoted *unsigned int* type of **x**. Converting the constant -1 to an *unsigned int* results in the largest possible value representable in *unsigned int.* Obviously **x** is less than the largest possible *unsigned int*, and so foo() would have returned 0.

There’s a really interesting section in the ANSI C rationale that describes the issues the committee faced, in part 3.2.1.1. They decided upon the current integral promotion rule for unsigned types because they “were considered to be safer for the novice, or unwary, programmer”. In the end, there was no right answer for the committee, since either approach to unsigned integral promotion has problems. Arguably they made the least wrong decision in promoting unsigned integral types to the signed type *int*.

## Reference

The C++17 standard has multiple sections that involve integral promotion. For reference, here are the excerpts/summaries from the relevant parts of the C++17 standard draft http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf

7.6 Integral promotions [conv.prom]

1 A prvalue of an integer type other than bool, char16_t, char32_t, or wchar_t whose integer conversion rank (7.15) is less than the rank of int can be converted to a prvalue of type int if int can represent all the values of the source type; otherwise, the source prvalue can be converted to a prvalue of type unsigned int.

8 Expressions [expr]

11 Many binary operators that expect operands of arithmetic or enumeration type cause conversions […These are] called the usual arithmetic conversions.

[… If neither operand has scoped enumeration type, type long double, double, or float,] the integral promotions (7.6) shall be performed on both operands.

8.3.1 Unary operators [expr.unary.op] (parts 7, 8, 10)

[For the unary operators +, -, ~, the operands are subject to integral promotion.]

8.6 Multiplicative operators [expr.mul]

[Binary operators *, /, %]

2
The usual arithmetic conversions are performed on the operands and determine the type of the result.

8.7 Additive operators [expr.add]

1 The additive [binary] operators + and – group left-to-right. The usual arithmetic conversions are performed for operands of arithmetic or enumeration type.

8.8 Shift operators [expr.shift]

[For the binary operators << and >>, the operands are subject to integral promotion.]

8.9 Relational operators [expr.rel]

[<, <=, >, >=]

2 The usual arithmetic conversions are performed on operands of arithmetic or enumeration type

8.10 Equality operators [expr.eq]

[==, !=]

6 If both operands are of arithmetic or enumeration type, the usual arithmetic conversions are performed on both operands

8.11 Bitwise AND operator [expr.bit.and]

1 The usual arithmetic conversions are performed;

8.12 Bitwise exclusive OR operator [expr.xor]

1 The usual arithmetic conversions are performed;

8.13 Bitwise inclusive OR operator [expr.or]

1 The usual arithmetic conversions are performed;