Floating-Point and Integral Promotion

In an earlier lesson, we noted that C++ has minimum size guarantees for each fundamental type. However, the actual size of these types can vary based on compiler and architecture.

This variability was allowed so that the int and double data types could be set to the size that maximizes performance on a given architecture. For example, a 32-bit computer will typically be able to process 32-bits of data at a time. In such cases, an int would likely be set to a width of 32-bits, since this is the "natural" size of the data that the CPU operates on (and likely to be most performant).

Reminder
The number of bits a data type uses is called its width. A wider data type is one that uses more bits, and a narrower data type is one that uses fewer bits.

But what happens when we want our 32-bit CPU to modify an 8-bit value (such as a char) or a 16-bit value? Some 32-bit processors (such as 32-bit x86 CPUs) can manipulate 8-bit or 16-bit values directly. However, doing so is often slower than manipulating 32-bit values! Other 32-bit CPUs (like 32-bit PowerPC CPUs), can only operate on 32-bit values, and additional tricks must be employed to manipulate narrower values.

Numeric Promotion

Because C++ is designed to be portable and performant across a wide range of architectures, the language designers didn't want to assume a given CPU would be able to efficiently manipulate values narrower than the natural data size for that CPU.

To help address this challenge, C++ defines a category of type conversions informally called numeric promotions. A numeric promotion is the type conversion of certain narrower numeric types (such as a char) to certain wider numeric types (typically int or double) that can be processed efficiently.

All numeric promotions are value-preserving. A value-preserving conversion (also called a safe conversion) is one where every possible source value can be converted into an equal value of the destination type.

Because promotions are safe, the compiler will freely use numeric promotion as needed, and will not issue a warning when doing so.

Numeric Promotion Reduces Redundancy

Numeric promotion solves another problem as well. Consider the case where you wanted to write a function to display a value of type int:

#include <iostream>

void displayInt(int value)
{
    std::cout << value << '\n';
}

While this is straightforward, what happens if we want to also be able to display a value of type short, or type char? If type conversions didn't exist, we'd have to write a different display function for short and another one for char. And don't forget another version for unsigned char, signed char, unsigned short, wchar_t, char8_t, char16_t, and char32_t! You can see how this quickly becomes unmanageable.

Numeric promotion comes to the rescue here: we can write functions that have int and/or double parameters (such as the displayInt() function above). That same code can then be called with arguments of types that can be numerically promoted to match the types of the function parameters.

Numeric Promotion Categories

The numeric promotion rules are divided into two subcategories: integral promotions and floating point promotions. Only the conversions listed in these categories are considered to be numeric promotions.

Floating Point Promotions

We'll start with the easier one.

Using the floating point promotion rules, a value of type float can be converted to a value of type double.

This means we can write a function that takes a double and then call it with either a double or a float value:

#include <iostream>

void displayDouble(double value)
{
    std::cout << value << '\n';
}

int main()
{
    displayDouble(5.0); // no conversion necessary
    displayDouble(4.0f); // numeric promotion of float to double

    return 0;
}

In the second call to displayDouble(), the float literal 4.0f is promoted into a double, so that the argument type matches the function parameter type.

Integral Promotions

The integral promotion rules are more complicated.

Using the integral promotion rules, the following conversions can be made:

  • signed char or signed short can be converted to int
  • unsigned char, char8_t, and unsigned short can be converted to int if int can hold the entire range of the type, or unsigned int otherwise
  • If char is signed by default, it follows the signed char conversion rules above. If it is unsigned by default, it follows the unsigned char conversion rules above
  • bool can be converted to int, with false becoming 0 and true becoming 1

Assuming an 8 bit byte and an int size of 4 bytes or larger (which is typical these days), the above basically means that bool, char, signed char, unsigned char, signed short, and unsigned short all get promoted to int.

Advanced note: There are a few other integral promotion rules used less often. These can be found in the C++ reference documentation.

In most cases, this lets us write a function taking an int parameter, and then use it with a wide variety of other integral types. For example:

#include <iostream>

void displayInt(int value)
{
    std::cout << value << '\n';
}

int main()
{
    displayInt(2);

    short quantity{ 3 }; // there is no short literal suffix, so we'll use a variable
    displayInt(quantity); // numeric promotion of short to int

    displayInt('a'); // numeric promotion of char to int
    displayInt(true); // numeric promotion of bool to int

    return 0;
}

There are two things worth noting here. First, on some architectures (e.g. with 2 byte ints) it's possible for some of the unsigned integral types to be promoted to unsigned int rather than int.

Second, some narrower unsigned types (such as unsigned char) may be promoted to larger signed types (such as int). So while integral promotion is value-preserving, it doesn't necessarily preserve the signedness (signed/unsigned) of the type.

Not All Widening Conversions Are Numeric Promotions

Some widening type conversions (such as char to short, or int to long) are not considered to be numeric promotions in C++ (they are numeric conversions, which we'll cover in the next lesson). This is because such conversions don't assist in the goal of converting smaller types to larger types that can be processed more efficiently.

The distinction is mostly academic. However, in certain cases, the compiler will favor numeric promotions over numeric conversions. We'll see examples where this makes a difference when we cover function overload resolution in an upcoming lesson.

Summary

Numeric promotions: Type conversions of certain narrower numeric types to wider numeric types (typically int or double) that can be processed efficiently. All numeric promotions are value-preserving and safe.

Floating-point promotions: Convert float to double.

Integral promotions: Convert narrower integral types (bool, char, signed char, unsigned char, signed short, unsigned short) to int or unsigned int (depending on platform).

Value-preserving conversions: Conversions where every possible source value can be converted into an equal value of the destination type. Numeric promotions are always value-preserving.

Benefits: Numeric promotions enable writing functions with int or double parameters that work with arguments of many different numeric types, and allow processors to work with types in their most efficient data size.

Understanding numeric promotions is essential for writing efficient, flexible code and for grasping how C++ handles type conversions automatically during expression evaluation.