0%

Chapter10-Implicit-type-conversion

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void someFcn(int i)
{
}

int main()
{
double d{ 5.0 };

someFcn(d); // bad: implicit narrowing conversion will generate compiler warning

// good: we're explicitly telling the compiler this narrowing conversion is intentional
someFcn(static_cast<int>(d)); // no warning generated

return 0;
}

Brace initialization disallows narrowing conversions

1
2
3
4
5
6
int main()
{
int i { 3.5 }; // won't compile

return 0;
}

Some constexpr conversions aren’t considered narrowing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

void print(unsigned int u) // note: unsigned
{
std::cout << u << '\n';
}

int main()
{
std::cout << "Enter an integral value: ";
int n{};
std::cin >> n; // enter 5 or -5
print(n); // conversion to unsigned may or may not preserve value

return 0;
}

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
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>

int main()
{
constexpr int n1{ 5 }; // note: constexpr
unsigned int u1 { n1 }; // okay: conversion is not narrowing due to exclusion clause

constexpr int n2 { -5 }; // note: constexpr
unsigned int u2 { n2 }; // compile error: conversion is narrowing due to value change

return 0;
}

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
#include <iostream>

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
2
3
4
5
6
7
8
9
#ifdef INT_2_BYTES
using int8_t = char;
using int16_t = int;
using int32_t = long;
#else
using int8_t = char;
using int16_t = short;
using int32_t = int;
#endif

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
8
int 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
11
int 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
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

void foo()
{
}

int main()
{
auto a; // The compiler is unable to deduce the type of a
auto b { }; // The compiler is unable to deduce the type of b
auto c { foo() }; // Invalid: c can't have type incomplete type void

return 0;
}

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
7
int 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;
}
## Type deduction for string literals

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
#include <string>
#include <string_view>

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
2
3
4
5
6
7
8
9
10
int main()
{
constexpr double a { 3.4 }; // a has type const double (constexpr not part of type, const is implicit)

auto b { a }; // b has type double (const dropped)
const auto c { a }; // c has type const double (const dropped but reapplied)
constexpr auto d { a }; // d has type const double (const dropped but implicitly reapplied by constexpr)

return 0;
}

Type deduction for functions

1
2
3
4
auto add(int x, int y)
{
return x + 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
2
3
4
5
6
7
auto someFcn(bool b)
{
if (b)
return 5; // return type int
else
return 6.7; // return type double
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

auto foo();

int main()
{
std::cout << foo() << '\n'; // the compiler has only seen a forward declaration at this point

return 0;
}

auto foo()
{
return 5;
}
//use of ‘auto foo()’ before deduction of ‘auto’
    Favor explicit return types over function return type deduction for normal functions.

Trailing return type syntax

1
2
3
4
auto add(int x, int y) -> int
{
return (x + y);
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>

void addAndPrint(auto x, auto y)
{
std::cout << x + y << '\n';
}

int main()
{
addAndPrint(2, 3); // case 1: call addAndPrint with int parameters
addAndPrint(4.5, 6.7); // case 2: call addAndPrint with double parameters

return 0;
}

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.