In the previous lesson (10.2 -- Floating-point and integral promotion), we covered numeric promotions, which are conversions of specific narrower numeric types to wider numeric types (typically int
or double
) that can be processed efficiently.
C++ supports another category of numeric type conversions, called numeric conversions. These numeric conversions cover additional type conversions between fundamental types.
Key insight
Any type conversion covered by the numeric promotion rules (10.2 -- Floating-point and integral promotion) is called a numeric promotion, not a numeric conversion.
There are five basic types of numeric conversions.
- Converting an integral type to any other integral type (excluding integral promotions):
- Converting a floating point type to any other floating point type (excluding floating point promotions):
- Converting a floating point type to any integral type:
- Converting an integral type to any floating point type:
- Converting an integral type or a floating point type to a bool:
As an aside…
Because brace initialization strictly disallows some types of numeric conversions (more on this next lesson), we use copy initialization in this lesson (which does not have any such limitations) in order to keep the examples simple.
Safe and unsafe conversions
Unlike numeric promotions (which are always value-preserving and thus “safe”), many numeric conversions are unsafe. An unsafe conversion is one where at least one value of the source type cannot be converted into an equal value of the destination type.
Numeric conversions fall into three general safety categories:
- Value-preserving conversions are safe numeric conversions where the destination type can exactly represent all possible values in the source type.
For example, int
to long
and short
to double
are safe conversions, as the source value can always be converted to an equal value of the destination type.
Compilers will typically not issue warnings for implicit value-preserving conversions.
A value converted using a value-preserving conversion can always be converted back to the source type, resulting in a value that is equivalent to the original value:
- Reinterpretive conversions are unsafe numeric conversions where the converted value may be different than the source value, but no data is lost. Signed/unsigned conversions fall into this category.
For example, when converting a signed int
to an unsigned int
:
In the case of u1
, the signed int value 5
is converted to unsigned int value 5
. Thus, the value is preserved in this case.
In the case of u2
, the signed int value -5
is converted to an unsigned int. Since an unsigned int can’t represent negative numbers, the result will be modulo wrapped to a large integral value that is outside the range of a signed int. The value is not preserved in this case.
Such value changes are typically undesirable, and will often cause the program to exhibit unexpected or implementation-defined behavior.
Related content
We discuss how out-of-range values are converted between signed and unsigned types in lesson 4.12 -- Introduction to type conversion and static_cast.
Warning
Even though reinterpretive conversions are unsafe, most compilers leave implicit signed/unsigned conversion warnings disabled by default.
This is because in some areas of modern C++ (such as when working with standard library arrays), signed/unsigned conversions can be hard to avoid. And practically speaking, the majority of such conversions do not actually result in a value change. Therefore, enabling such warnings can lead to many spurious warnings for signed/unsigned conversions that are actually okay (drowning out legitimate warnings).
If you choose to leave such warnings disabled, be extra careful of inadvertent conversions between these types (particularly when passing an argument to a function taking a parameter of the opposite sign).
Values converted using a reinterpretive conversion can be converted back to the source type, resulting in a value equivalent to the original value (even if the initial conversion produced a value out of range of the source type). As such, reinterpretive conversions do not lose data during the conversion process.
For advanced readers
Prior to C++20, converting an unsigned value that is out-of-range of the signed value is technically implementation-defined behavior (due to the allowance that signed integers could use a different binary representation than unsigned integers). In practice, this was a non-issue on modern system.
C++20 now requires that signed integers use 2s complement. As a result, the conversion rules were changed so that the above is now well-defined as a reinterpretive conversion (an out-of-range conversion will produce modulo wrapping).
Note that while such conversions are well defined, signed arithmetic overflow (which occurs when an arithmetic operation produces a value outside the range that can be stored) is still undefined behavior.
- Lossy conversions are unsafe numeric conversions where data may be lost during the conversion.
For example, double
to int
is a conversion that may result in data loss:
Conversion from double
to float
can also result in data loss:
Converting a value that has lost data back to the source type will result in a value that is different than the original value:
For example, if double
value 3.5
is converted to int
value 3
, the fractional component 0.5
is lost. When 3
is converted back to a double,
the result is 3.0
, not 3.5
.
Compilers will generally issue a warning (or in some cases, an error) when an implicit lossy conversion would be performed at runtime.
Warning
Some conversions may fall into different categories depending on the platform.
For example, int
to double
is typically a safe conversion, because int
is usually 4 bytes and double
is usually 8 bytes, and on such systems, all possible int
values can be represented as a double
. However, there are architectures where both int
and double
are 8 bytes. On such architectures, int
to double
is a lossy conversion!
We can demonstrate this by casting a long long value (which must be at least 64 bits) to double and back:
This prints:
10000000000000000
Note that our last digit has been lost!
Unsafe conversions should be avoided as much as possible. However, this is not always possible. When unsafe conversions are used, it is most often when:
- We can constrain the values to be converted to just those that can be converted to equal values. For example, an
int
can be safely converted to anunsigned int
when we can guarantee that theint
is non-negative. - We don’t mind that some data is lost because it is not relevant. For example, converting an
int
to abool
results in the loss of data, but we’re typically okay with this because we’re just checking if theint
has value0
or not.
More on numeric conversions
The specific rules for numeric conversions are complicated and numerous, so here are the most important things to remember.
- In all cases, converting a value into a type whose range doesn’t support that value will lead to results that are probably unexpected. For example:
In this example, we’ve assigned a large integer to a variable with type char
(that has range -128 to 127). This causes the char to overflow, and produces an unexpected result:
48
- Remember that overflow is well-defined for unsigned values and produces undefined behavior for signed values.
- Converting from a larger integral or floating point type to a smaller type from the same family will generally work so long as the value fits in the range of the smaller type. For example:
This produces the expected result:
2 0.1234
- In the case of floating point values, some rounding may occur due to a loss of precision in the smaller type. For example:
In this case, we see a loss of precision because the float
can’t hold as much precision as a double
:
0.123456791
- Converting from an integer to a floating point number generally works as long as the value fits within the range of the floating point type. For example:
This produces the expected result:
10
- Converting from a floating point to an integer works as long as the value fits within the range of the integer, but any fractional values are lost. For example:
In this example, the fractional value (.5) is lost, leaving the following result:
3
While the numeric conversion rules might seem scary, in reality the compiler will generally warn you if you try to do something dangerous (excluding some signed/unsigned conversions).