We introduced type conversion in lesson 4.12 -- Introduction to type conversion and static_cast. To recap the most important points from that lesson:
- The process of converting data from one type to another type is called “type conversion”.
- Implicit type conversion is performed automatically by the compiler when one data type is required, but a different data type is supplied.
- Explicit type conversion is requested by using a cast operator, such as
static_cast
. - Conversions do not change the data being converted. Instead, the conversion process uses that data as input, and produces the converted result.
- When converting a value to another type of value, the conversion process produces a temporary object of the target type that holds the result of the conversion.
In the first half of this chapter, we’re going to dig a bit deeper into how type conversion works. We’ll start with implicit conversions in this lesson, and explicit type conversions (casting) in upcoming lesson 10.6 -- Explicit type conversion (casting) and static_cast. Since type conversion is used all over the place, having some understanding of what’s happening under the hood when a conversion is needed is important. This knowledge is also relevant when for understanding how overloaded functions (functions that can have the same name as other functions) work.
Author’s note
In this chapter, we’ll focus on the conversion of values to other types of values. We’ll cover other types of conversions once we introduce the prerequisite topics (such as pointers, references, inheritance, etc…).
Why conversions are needed
The value of an object is stored as a sequence of bits, and the data type of the object tells the compiler how to interpret those bits into meaningful values. Different data types may represent the “same” value differently. For example, the integer value 3
might be stored as binary 0000 0000 0000 0000 0000 0000 0000 0011
, whereas floating point value 3.0
might be stored as binary 0100 0000 0100 0000 0000 0000 0000 0000
.
So what happens when we do something like this?
float f{ 3 }; // initialize floating point variable with int 3
In such a case, the compiler can’t just copy the bits used to represent int
value 3
into the memory allocated for float
variable f
. If it were to do so, then when f
(which has type float
) was evaluated, those bits would be interpreted as a float
rather than an int
, and who knows what float
value we’d end up with!
As an aside…
The following program actually prints int
value 3
as if it were a float
:
#include <iostream>
#include <cstring>
int main()
{
int n { 3 }; // here's int value 3
float f {}; // here's our float variable
std::memcpy(&f, &n, sizeof(float)); // copy the bits from n into f
std::cout << f << '\n'; // print f (containing the bits from n)
return 0;
}
This produces the following result:
4.2039e-45
Instead, the integer value 3
needs to be converted into the equivalent floating point value 3.0
, which can then be stored in the memory allocated for f
(using the bit representation for float
value 3.0
) .
When implicit type conversion happens
Implicit type conversion (also called automatic type conversion or coercion) is performed automatically by the compiler when an expression of some type is supplied in a context where some other type is expected. The vast majority of type conversions in C++ are implicit type conversions. For example, implicit type conversion happens in all of the following cases:
When initializing (or assigning a value to) a variable with a value of a different data type:
double d{ 3 }; // int value 3 implicitly converted to type double
d = 6; // int value 6 implicitly converted to type double
When the type of a return value is different from the function’s declared return type:
float doSomething()
{
return 3.0; // double value 3.0 implicitly converted to type float
}
When using certain binary operators with operands of different types:
double division{ 4.0 / 3 }; // int value 3 implicitly converted to type double
When using a non-Boolean value in an if-statement:
if (5) // int value 5 implicitly converted to type bool
{
}
When an argument passed to a function is a different type than the function parameter:
void doSomething(long l)
{
}
doSomething(3); // int value 3 implicitly converted to type long
So how does the compiler know how to convert a value to a different type anyway?
The standard conversions
As part of the core language, the C++ standard defines a collection of conversion rules known as the “standard conversions”. The standard conversions specify how various fundamental types (and certain compound types, including arrays, references, pointers, and enumerations) convert to other types within that same group.
As of C++23, there are 14 different standard conversions. These can be roughly grouped into 5 general categories:
Category | Meaning | Link |
---|---|---|
Numeric promotions | Conversions of small integral types to int or unsigned int , and of float to double . |
10.2 -- Floating-point and integral promotion |
Numeric conversions | Other integral and floating point conversions that aren’t promotions. | 10.3 -- Numeric conversions |
Qualification conversions | Conversions that add or remove const or volatile . |
|
Value transformations | Conversions that change the value category of an expression | 12.2 -- Value categories (lvalues and rvalues) |
Pointer conversions | Conversions from std::nullptr to pointer types, or pointer types to other pointer types |
For example, converting an int
value to a float
value falls under the numeric conversions category, so the compiler to perform such a conversion, the compiler simply need apply the int
to float
numeric conversion rules.
The numeric conversions and numeric promotions are the most important of these categories, and we’ll cover them in more detail in upcoming lessons.
For advanced readers
Here is the full list of standard conversions:
Category | Standard Conversion | Description | Also See |
---|---|---|---|
Value transformation | Lvalue-to-rvalue | Converts lvalue expression to rvalue expression | 12.2 -- Value categories (lvalues and rvalues) |
Value transformation | Array-to-pointer | Converts C-style array to pointer to first array element (a.k.a. array decay) | 17.8 -- C-style array decay |
Value transformation | Function-to-pointer | Converts function to function pointer | 20.1 -- Function Pointers |
Value transformation | Temporary materialization | Converts value to temporary object | |
Qualification conversion | Qualification conversion | Adds or removes const or volatile from types |
|
Numeric promotions | Integral promotions | Converts smaller integral types to int or unsigned int |
10.2 -- Floating-point and integral promotion |
Numeric promotions | Floating point promotions | Converts float to double |
10.2 -- Floating-point and integral promotion |
Numeric conversions | Integral conversions | Integral conversions that aren’t integral promotions | 10.3 -- Numeric conversions |
Numeric conversions | Floating point conversions | Floating point conversions that aren’t floating point promotions | 10.3 -- Numeric conversions |
Numeric conversions | Integral-floating conversions | Converts integral and floating point types | 10.3 -- Numeric conversions |
Numeric conversions | Boolean conversions | Converts integral, unscoped enumeration, pointer, or pointer-to-memver to bool | 4.10 -- Introduction to if statements |
Pointer conversions | Pointer conversions | Converts std::nullptr to pointer, or pointer to void pointer or base class |
|
Pointer conversions | Pointer-to-member conversions | Converts std::nullptr to pointer-to-memberor pointer-to-member of base class to pointer-to-member of derived class |
|
Pointer conversions | Function pointer conversions | Converts pointer-to-noexcept-function to pointer-to-function |
Type conversion can fail
When a type conversion is invoked (whether implicitly or explicitly), the compiler will determine whether it can convert the value from the current type to the desired type. If a valid conversion can be found, then the compiler will produce a new value of the desired type.
If the compiler can’t find an acceptable conversion, then the compilation will fail with a compile error. Type conversions can fail for any number of reasons. For example, the compiler might not know how to convert a value between the original type and the desired type.
For example:
int main()
{
int x { "14" };
return 0;
}
The because there isn’t a standard conversion from the string literal “14” to int
, the compiler will produce an error. For example, GCC produces the error: prog.cc:3:13: error: invalid conversion from 'const char*' to 'int' [-fpermissive]
.
In other cases, specific features may disallow some categories of conversions. For example:
int x { 3.5 }; // brace-initialization disallows conversions that result in data loss
Even though the compiler knows how to convert a double
value to an int
value, narrowing conversions are disallowed when using brace-initialization.
There are also cases where the compiler may not be able to figure out which of several possible type conversions is the best one to use. We’ll see examples of this in lesson 11.3 -- Function overload resolution and ambiguous matches.
The full set of rules describing how type conversions work is both lengthy and complicated, and for the most part, type conversion “just works”. In the next set of lessons, we’ll cover the most important things you need to know about the standard conversions. If finer detail is required for some uncommon case, the full rules are detailed in technical reference documentation for implicit conversions.
Let’s get to it!