The process of producing a new value of some type from a value of a different type is called a conversion.
Conversions do not change the value or type being converted. Instead, a new value with the desired type is created as a result of the conversion
Implicit type conversion
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
1 | int x { 3.5 }; // brace-initialization disallows conversions that result in data loss |
The standard conversions
- Numeric promotions
- Numeric conversions
- Arithmetic conversions
- Other conversions # Floating-point and integral promotion
Numeric promotion
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
The numeric promotion rules are divided into two subcategories: integral promotions and floating point promotions ### integral promotions
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.
While integral promotion is value-preserving, ==it does not necessarily preserve the signedness (signed/unsigned) of the type==. ### floating point promotions
Using the floating point promotion rules, a value of type float can be converted to a value of type double.
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
Safe and unsafe conversions
Value-preserving conversions are safe numeric conversions where the destination type can exactly represent all possible values in the source type.
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.
Lossy conversions are unsafe numeric conversions where data may be lost during the conversion.
Narrowing conversions, list initialization, and constexpr initializers
Narrowing coversions
In C++, a narrowing conversion is a potentially unsafe numeric conversion where the destination type may not be able to hold all the values of the source type.
The following conversions are defined to be narrowing:
- From a floating point type to an integral type.
- From a floating point type to a narrower or lesser ranked floating point type, unless the value being converted is constexpr and is in range of the destination type (even if the destination type doesn’t have the precision to store all the significant digits of the number).
- From an integral to a floating point type, unless the value being converted is constexpr and whose value can be stored exactly in the destination type.
- From an integral type to another integral type that cannot represent all values of the original type, unless the value being converted is constexpr and whose value can be stored exactly in the destination type. This covers both wider to narrower integral conversions, as well as integral sign conversions (signed to unsigned, or vice-versa).
Make intentional narrowing conversions explicit
Use static_cast
when narrowing conversions is needed.Doing so helps document that the narrowing conversion is intentional, and will suppress any compiler warnings or errors that would otherwise result.
1 | void someFcn(int i) |
Brace initialization disallows narrowing conversions
1 | int main() |
Some constexpr conversions aren’t considered narrowing
1 |
|
compilier raises conversion to ‘unsigned int’ from ‘int’ may change the sign of the result .
When the source value of a narrowing conversion is constexpr, the specific value to be converted must be known to the compiler. In such cases, the compiler can perform the conversion itself, and then check whether the value was preserved. If the value was not preserved, the compiler can halt compilation with an error. If the value is preserved, the conversion is not considered to be narrowing (and the compiler can replace the entire conversion with the converted result, knowing that doing so is safe).
1 |
|
Arithmetic conversions
Typedefs and type aliases
In C++, using is a keyword that creates an alias for an existing data type
example: 1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
using Distance = double; // define Distance as an alias for type double
Distance milesToDestination{ 3.4 }; // defines a variable of type double
std::cout << milesToDestination << '\n'; // prints a double value
return 0;
}
Naming type aliases
- Type aliases that end in a “_t” suffix (the “_t” is short for “type”). This convention is often used by the standard library for globally scoped type names (like size_t and nullptr_t).
- Type aliases that end in a “_type” suffix. This convention is used by some standard library types (like std::string) to name nested type aliases (e.g. std::string::size_type).
- Type aliases that use no suffix.
In modern C++, the convention is to name type aliases (or any other type) that you define yourself starting with a capital letter, and using no suffix.
Type aliases are not distinct types
An alias does not actually define a new, distinct type (one that is considered separate from other types) -- it just introduces a new identifier for an existing type.
The scope of a type alias
Because scope is a property of an identifier, type alias identifiers follow the same scoping rules as variable identifiers: a type alias defined inside a block has block scope and is usable only within that block, whereas a type alias defined in the global namespace has global scope and is usable to the end of the file.
Typedefs
Prefer type aliases over typedefs.
When should we use type aliases
Using type aliases for platform independent coding
1 |
|
Using type aliases to make complex types easier to read
Using type aliases to document the meaning of a value
Using type aliases for easier code maintenance
Type deduction for objects using the auto keyword
Type deduction for initialized variables
Type deduction (also sometimes called type inference) is a feature that allows the compiler to deduce the type of an object from the object’s initializer
example: 1
2
3
4
5
6
7
8int main()
{
auto d { 5.0 }; // 5.0 is a double literal, so d will be deduced as a double
auto i { 1 + 2 }; // 1 + 2 evaluates to an int, so i will be deduced as an int
auto x { i }; // i is an int, so x will be deduced as an int
return 0;
}
Because function calls are valid expressions, we can even use type deduction when our initializer is a non-void function call: 1
2
3
4
5
6
7
8
9
10
11int add(int x, int y)
{
return x + y;
}
int main()
{
auto sum { add(5, 6) }; // add() returns an int, so sum's type will be deduced as an int
return 0;
}
Type deduction must have something to deduce from
Type deduction will not work for objects that either do not have initializers or have empty initializers. It also will not work when the initializer has type void (or any other incomplete type).
1 |
|
Type deduction drops const from the deduced type
In most cases, type deduction will drop the const from deduced types. For example: 1
2
3
4
5
6
7int main()
{
const int a { 5 }; // a has type const int
auto b { a }; // b has type int (const dropped)
//const auto b { a }; // b has type const int (const dropped but reapplied)
return 0;
}
1 | auto s { "Hello, world" }; // s will be type const char*, not std::string |
If you want the type deduced from a string literal to be std::string or std::string_view, you’ll need to use the s or sv literal suffixes. 1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
using namespace std::literals; // easiest way to access the s and sv suffixes
auto s1 { "goo"s }; // "goo"s is a std::string literal, so s1 will be deduced as a std::string
auto s2 { "moo"sv }; // "moo"sv is a std::string_view literal, so s2 will be deduced as a std::string_view
return 0;
}
Type deduction and constexpr
Because constexpr is not part of the type system, it cannot be deduced as part of type deduction. However, a constexpr variable is implicitly const, and this const will be dropped during type deduction (and can be readded if desired):
1 | int main() |
Type deduction for functions
1 | auto add(int x, int y) |
When using an auto return type, all return statements within the function must return values of the same type, otherwise an error will result.
1 | auto someFcn(bool b) |
Functions that use an auto return type must be fully defined before they can be used (a forward declaration is not sufficient). For example:
1 |
|
Favor explicit return types over function return type deduction for normal functions.
Trailing return type syntax
1 | auto add(int x, int y) -> int |
In this case, auto does not perform type deduction -- it is just part of the syntax to use a trailing return type.
Type deduction can’t be used for function parameter types
1 |
|
Unfortunately, type deduction doesn’t work for function parameters, and prior to C++20, the above program won’t compile.
In C++20, the auto keyword was extended so that the above program will compile and function correctly -- however, auto is not invoking type deduction in this case. Rather, it is triggering a different feature called function templates that was designed to actually handle such cases.