Skip to content

4.11. Type Conversions

Fundamental

In C++ some types are related to each other. When two types are related, we can use an object or value of one type where an operand of the related type is expected. Two types are related if there is a conversion between them.

As an example, consider the following expression, which initializes ival to 6:

c++
int ival = 3.541 + 3; // the compiler might warn about loss of precision

The operands of the addition are values of two different types: 3.541 has type double, and 3 is an int. Rather than attempt to add values of the two different types, C++ defines a set of conversions to transform the operands to a common type. These conversions are carried out automatically without programmer intervention—and sometimes without programmer knowledge. For that reason, they are referred to as implicit conversions.

The implicit conversions among the arithmetic types are defined to preserve precision, if possible. Most often, if an expression has both integral and floatingpoint operands, the integer is converted to floating-point. In this case, 3 is converted to double, floating-point addition is done, and the result is a double.

The initialization happens next. In an initialization, the type of the object we are initializing dominates. The initializer is converted to the object’s type. In this case, the double result of the addition is converted to int and used to initialize ival. Converting a double to an int truncates the double’s value, discarding the decimal portion. In this expression, the value 6 is assigned to ival.

When Implicit Conversions Occur

The compiler automatically converts operands in the following circumstances:

  • In most expressions, values of integral types smaller than int are first promoted to an appropriate larger integral type.
  • In conditions, nonbool expressions are converted to bool.
  • In initializations, the initializer is converted to the type of the variable; in assignments, the right-hand operand is converted to the type of the left-hand.
  • In arithmetic and relational expressions with operands of mixed types, the types are converted to a common type.
  • As we’ll see in Chapter 6, conversions also happen during function calls.

4.11.1. The Arithmetic Conversions

Fundamental

The arithmetic conversions, which we introduced in § 2.1.2 (p. 35), convert one arithmetic type to another. The rules define a hierarchy of type conversions in which operands to an operator are converted to the widest type. For example, if one operand is of type long double, then the other operand is converted to type long double regardless of what the second type is. More generally, in expressions that mix floating-point and integral values, the integral value is converted to an appropriate floating-point type.

Integral Promotions

The integral promotions convert the small integral types to a larger integral type. The types bool, char, signed char, unsigned char, short, and unsigned short are promoted to int if all possible values of that type fit in an int. Otherwise, the value is promoted to unsigned int. As we’ve seen many times, a bool that is false promotes to 0 and true to 1.

The larger char types (wchar_t, char16_t, and char32_t) are promoted to the smallest type of int, unsigned int, long, unsigned long, long long, or unsigned long long in which all possible values of that character type fit.

Operands of Unsigned Type

If the operands of an operator have differing types, those operands are ordinarily converted to a common type. If any operand is an unsigned type, the type to which the operands are converted depends on the relative sizes of the integral types on the machine.

As usual, integral promotions happen first. If the resulting type(s) match, no further conversion is needed. If both (possibly promoted) operands have the same signedness, then the operand with the smaller type is converted to the larger type.

When the signedness differs and the type of the unsigned operand is the same as or larger than that of the signed operand, the signed operand is converted to unsigned. For example, given an unsigned int and an int, the int is converted to unsigned int. It is worth noting that if the int has a negative value, the result will be converted as described in § 2.1.2 (p. 35), with the same results.

The remaining case is when the signed operand has a larger type than the unsigned operand. In this case, the result is machine dependent. If all values in the unsigned type fit in the larger type, then the unsigned operand is converted to the signed type. If the values don’t fit, then the signed operand is converted to the unsigned type. For example, if the operands are long and unsigned int, and int and long have the same size, the long will be converted to unsigned int. If the long type has more bits, then the unsigned int will be converted to long.

Understanding the Arithmetic Conversions

One way to understand the arithmetic conversions is to study lots of examples:

c++
bool      flag;         char           cval;
short     sval;         unsigned short usval;
int       ival;         unsigned int   uival;
long      lval;         unsigned long  ulval;
float     fval;         double         dval;
3.14159L + 'a'; //  'a' promoted to int, then that int converted to long double
dval + ival;    //  ival converted to double
dval + fval;    //  fval converted to double
ival = dval;    //  dval converted (by truncation) to int
flag = dval;    //  if dval is 0, then flag is false, otherwise true
cval + fval;    //  cval promoted to int, then that int converted to float
sval + cval;    //  sval and cval promoted to int
cval + lval;    //  cval converted to long
ival + ulval;   //  ival converted to unsigned long
usval + ival;   //  promotion depends on the size of unsigned short and int
uival + lval;   //  conversion depends on the size of unsigned int and long

In the first addition, the character constant lowercase 'a' has type char, which is a numeric value (§ 2.1.1, p. 32). What that value is depends on the machine’s character set. On our machine, 'a' has the numeric value 97. When we add 'a' to a long double, the char value is promoted to int, and then that int value is converted to a long double. The converted value is added to the literal. The other interesting cases are the last two expressions involving unsigned values. The type of the result in these expressions is machine dependent.

INFO

Exercises Section 4.11.1

Exercise 4.34: Given the variable definitions in this section, explain what conversions take place in the following expressions:

(a) if (fval)

(b) dval = fval + ival;

(c) dval + ival * cval;

Remember that you may need to consider the associativity of the operators.

Exercise 4.35: Given the following definitions,

c++
char cval;     int ival;    unsigned int ui;
float fval;    double dval;

identify the implicit type conversions, if any, taking place:

(a)cval = 'a' + 3;

(b)fval = ui - ival * 1.0;

(c)dval = ui * fval;

(d)cval = ival + fval + dval;

4.11.2. Other Implicit Conversions

Fundamental

In addition to the arithmetic conversions, there are several additional kinds of implicit conversions. These include:

Array to Pointer Conversions: In most expressions, when we use an array, the array is automatically converted to a pointer to the first element in that array:

c++
int ia[10];   // array of ten ints
int* ip = ia; // convert ia to a pointer to the first element

This conversion is not performed when an array is used with decltype or as the operand of the address-of (&), sizeof, or typeid (which we’ll cover in § 19.2.2 (p. 826)) operators. The conversion is also omitted when we initialize a reference to an array (§ 3.5.1, p. 114). As we’ll see in § 6.7 (p. 247), a similar pointer conversion happens when we use a function type in an expression.

Pointer Conversions: There are several other pointer conversions: A constant integral value of 0 and the literal nullptr can be converted to any pointer type; a pointer to any nonconst type can be converted to void*, and a pointer to any type can be converted to a const void*. We’ll see in § 15.2.2 (p. 597) that there is an additional pointer conversion that applies to types related by inheritance.

Conversions to bool: There is an automatic conversion from arithmetic or pointer types to bool. If the pointer or arithmetic value is zero, the conversion yields false; any other value yields true:

c++
char *cp = get_string();
if (cp) /* ... */      // true if the pointer cp is not zero
while (*cp) /* ...  */ // true if *cp is not the null character

Conversion to const: We can convert a pointer to a nonconst type to a pointer to the corresponding const type, and similarly for references. That is, if T is a type, we can convert a pointer or a reference to T into a pointer or reference to const T, respectively (§ 2.4.1, p. 61, and § 2.4.2, p. 62):

c++
int i;
const int &j = i;  // convert a nonconst to a reference to const int
const int *p = &i; // convert address of a nonconst to the address of a const
int &r = j, *q = p; // error: conversion from const to nonconst not allowed

The reverse conversion—removing a low-level const—does not exist.

Conversions Defined by Class Types: Class types can define conversions that the compiler will apply automatically. The compiler will apply only one class-type conversion at a time. In § 7.5.4 (p. 295) we’ll see an example of when multiple conversions might be required, and will be rejected.

Our programs have already used class-type conversions: We use a class-type conversion when we use a C-style character string where a library string is expected (§ 3.5.5, p. 124) and when we read from an istream in a condition:

c++
string s, t = "a value";  // character string literal converted to type string
while (cin >> s)          // while condition converts cin to bool

The condition (cin >> s) reads cin and yields cin as its result. Conditions expect a value of type bool, but this condition tests a value of type istream. The IO library defines a conversion from istream to bool. That conversion is used (automatically) to convert cin to bool. The resulting bool value depends on the state of the stream. If the last read succeeded, then the conversion yields true. If the last attempt failed, then the conversion to bool yields false.

4.11.3. Explicit Conversions

Sometimes we want to explicitly force an object to be converted to a different type. For example, we might want to use floating-point division in the following code:

c++
int i, j;
double slope = i/j;

To do so, we’d need a way to explicitly convert i and/or j to double. We use a cast to request an explicit conversion.

WARNING

Although necessary at times, casts are inherently dangerous constructs.

Named Casts

A named cast has the following form:

c++
cast-name<type>(expression);

where type is the target type of the conversion, and expression is the value to be cast. If type is a reference, then the result is an lvalue. The cast-name may be one of static_cast, dynamic_cast, const_cast, and reinterpret_cast. We’ll cover dynamic_cast, which supports the run-time type identification, in § 19.2 (p. 825). The cast-name determines what kind of conversion is performed.

static_cast

Any well-defined type conversion, other than those involving low-level const, can be requested using a static_cast. For example, we can force our expression to use floating-point division by casting one of the operands to double:

c++
// cast used to force floating-point division
double slope = static_cast<double>(j) / i;

A static_cast is often useful when a larger arithmetic type is assigned to a smaller type. The cast informs both the reader of the program and the compiler that we are aware of and are not concerned about the potential loss of precision. Compilers often generate a warning for assignments of a larger arithmetic type to a smaller type. When we do an explicit cast, the warning message is turned off.

A static_cast is also useful to perform a conversion that the compiler will not generate automatically. For example, we can use a static_cast to retrieve a pointer value that was stored in a void* pointer (§ 2.3.2, p. 56):

c++
void* p = &d;   // ok: address of any nonconst object can be stored in a void*
// ok: converts void* back to the original pointer type
double *dp = static_cast<double*>(p);

When we store a pointer in a void* and then use a static_cast to cast the pointer back to its original type, we are guaranteed that the pointer value is preserved. That is, the result of the cast will be equal to the original address value. However, we must be certain that the type to which we cast the pointer is the actual type of that pointer; if the types do not match, the result is undefined.

const_cast

A const_cast changes only a low-level (§ 2.4.3, p. 63) const in its operand:

c++
const char *pc;
char *p = const_cast<char*>(pc); // ok: but writing through p is undefined

Conventionally we say that a cast that converts a const object to a nonconst type “casts away the const.” Once we have cast away the const of an object, the compiler will no longer prevent us from writing to that object. If the object was originally not a const, using a cast to obtain write access is legal. However, using a const_cast in order to write to a const object is undefined.

Only a const_cast may be used to change the constness of an expression. Trying to change whether an expression is const with any of the other forms of named cast is a compile-time error. Similarly, we cannot use a const_cast to change the type of an expression:

c++
const char *cp;
// error: static_cast can't cast away const
char *q = static_cast<char*>(cp);
static_cast<string>(cp); // ok: converts string literal to string
const_cast<string>(cp);  // error: const_cast only changes constness

A const_cast is most useful in the context of overloaded functions, which we’ll describe in § 6.4 (p. 232).

reinterpret_cast

A reinterpret_cast generally performs a low-level reinterpretation of the bit pattern of its operands. As an example, given the following cast

c++
int *ip;
char *pc = reinterpret_cast<char*>(ip);

we must never forget that the actual object addressed by pc is an int, not a character. Any use of pc that assumes it’s an ordinary character pointer is likely to fail at run time. For example:

c++
string str(pc);

is likely to result in bizarre run-time behavior.

The use of pc to initialize str is a good example of why reinterpret_cast is dangerous. The problem is that types are changed, yet there are no warnings or errors from the compiler. When we initialized pc with the address of an int, there is no error or warning from the compiler because we explicitly said the conversion was okay. Any subsequent use of pc will assume that the value it holds is a char*. The compiler has no way of knowing that it actually holds a pointer to an int. Thus, the initialization of str with pc is absolutely correct—albeit in this case meaningless or worse! Tracking down the cause of this sort of problem can prove extremely difficult, especially if the cast of ip to pc occurs in a file separate from the one in which pc is used to initialize a string.

WARNING

A reinterpret_cast is inherently machine dependent. Safely using reinterpret_cast requires completely understanding the types involved as well as the details of how the compiler implements the cast.

Old-Style Casts

In early versions of C++, an explicit cast took one of the following two forms:

c++
type (expr); // function-style cast notation
(type) expr; // C-language-style cast notation

INFO

Advice: Avoid Casts

Casts interfere with normal type checking (§ 2.2.2, p. 46). As a result, we strongly recommend that programmers avoid casts. This advice is particularly applicable to reinterpret_casts. Such casts are always hazardous. A const_cast can be useful in the context of overloaded functions, which we’ll cover in § 6.4 (p. 232). Other uses of const_cast often indicate a design flaw. The other casts, static_cast and dynamic_cast, should be needed infrequently. Every time you write a cast, you should think hard about whether you can achieve the same result in a different way. If the cast is unavoidable, errors can be mitigated by limiting the scope in which the cast value is used and by documenting all assumptions about the types involved.

Depending on the types involved, an old-style cast has the same behavior as a const_cast, a static_cast, or a reinterpret_cast. When we use an old-style cast where a static_cast or a const_cast would be legal, the old-style cast does the same conversion as the respective named cast. If neither cast is legal, then an old-style cast performs a reinterpret_cast. For example:

c++
char *pc = (char*) ip; // ip is a pointer to int

has the same effect as using a reinterpret_cast.

WARNING

Old-style casts are less visible than are named casts. Because they are easily overlooked, it is more difficult to track down a rogue cast.

INFO

Exercises Section 4.11.3

Exercise 4.36: Assuming i is an int and d is a double write the expression i *= d so that it does integral, rather than floating-point, multiplication.

Exercise 4.37: Rewrite each of the following old-style casts to use a named cast:

c++
int i;  double d;  const string *ps;  char *pc;  void *pv;

(a)pv = (void*)ps;

(b)i = int(*pc);

(c)pv = &d;

(d)pc = (char*) pv;

Exercise 4.38: Explain the following expression:

c++
double slope = static_cast<double>(j/i);