Explicit Type Conversion: Casting and static_cast

In our lesson on implicit type conversion, we learned that the compiler automatically converts values between types when needed. This works well when promoting values to wider types.

Many C++ beginners attempt something like:

double result = 20 / 8; // performs integer division, assigns 2.0 to result

Since 20 and 8 are both int, integer division occurs, evaluating to int value 2. This value then converts to double value 2.0 for assignment. Probably not the intended outcome.

For literals, replacing one or both with double literals enables floating-point division:

double result = 20.0 / 8.0; // performs floating-point division, assigns 2.5 to result

But what about variables?

int numerator { 20 };
int denominator { 8 };
double result = numerator / denominator; // integer division, assigns 2.0

Integer division happens here, giving result the value 2.0. Literal suffixes don't work with variables. We need to convert one (or both) operands to floating-point type for floating-point division.

C++ provides several type casting operators (commonly called casts) for explicit type conversion by the programmer. Since programmers explicitly request these casts, this is explicit type conversion (versus implicit type conversion where the compiler converts automatically).

Type Casting

C++ supports 5 cast types: static_cast, dynamic_cast, const_cast, reinterpret_cast, and C-style casts. The first four are named casts.

Advanced note: Cast types overview:

Cast Description Safe?
static_cast Compile-time conversions between related types Yes
dynamic_cast Runtime conversions on pointers/references in polymorphic hierarchies Yes
const_cast Adds or removes const Only for adding const
reinterpret_cast Reinterprets bit-level representation as another type No
C-style casts Combination of static_cast, const_cast, or reinterpret_cast No

Each cast takes an expression (evaluating to a value or object) and a target type as input. Output is the converted result.

We'll cover C-style casts and static_cast in this lesson (the most commonly used casts).

We discuss dynamic_cast after covering prerequisite topics.

const_cast and reinterpret_cast should generally be avoided as they're rarely useful and potentially harmful.

Warning
Avoid `const_cast` and `reinterpret_cast` unless absolutely necessary.

C-style Cast

In standard C programming, casting uses operator() with the target type inside parentheses and the value immediately after. In C++, this is a C-style cast. You may encounter these in C-converted code.

Example:

#include <iostream>

int main()
{
    int numerator { 20 };
    int denominator { 8 };

    std::cout << (double)numerator / denominator << '\n'; // C-style cast to double

    return 0;
}

We use a C-style cast to convert numerator to double. Since the left operand of operator/ now evaluates to floating-point, the right operand also converts to floating-point, enabling floating-point division.

C++ offers an alternative function-style cast resembling a function call:

    std::cout << double(numerator) / denominator << '\n'; // function-style cast to double

The function-style cast clarifies what's being converted (resembling a function argument).

C-style casts have significant problems in modern C++.

First, though a C-style cast appears singular, it can perform various conversions depending on usage, including static cast, const cast, or reinterpret cast (the latter two should be avoided). C-style casts don't clarify which cast(s) actually occur, making code harder to understand and opening doors to inadvertent misuse (thinking you're doing a simple cast but actually doing something dangerous). This often produces runtime-only errors.

Second, since C-style casts are just a type name, parenthesis, and variable/value, they're difficult to identify (making code harder to read) and even harder to search for.

Conversely, named casts are easy to spot and search for, make their purpose clear, have limited abilities, and produce compilation errors on misuse.

Best Practice
Avoid C-style casts.

Advanced note: C-style casts attempt these C++ casts in order:

  • const_cast
  • static_cast
  • static_cast followed by const_cast
  • reinterpret_cast
  • reinterpret_cast followed by const_cast

One unique C-style cast capability: converting derived objects to inaccessible base classes (e.g., from private inheritance).

static_cast for Most Value Conversions

By far the most used C++ cast is static cast, accessed via the static_cast keyword. static_cast explicitly converts values from one type to another.

You've seen static_cast convert char to int so std::cout prints it as an integer instead of character:

#include <iostream>

int main()
{
    char letter { 'z' };
    std::cout << static_cast<int>(letter) << '\n'; // prints 122 instead of z

    return 0;
}

To perform a static cast, we use the static_cast keyword, then place the target type in angled brackets. Inside parentheses goes the expression whose value converts. The syntax resembles a function call to static_cast<type>() with the expression as an argument. Static casting returns a temporary object direct-initialized with the converted value.

Here's how static_cast solves our introductory problem:

#include <iostream>

int main()
{
    int numerator { 20 };
    int denominator { 8 };

    std::cout << static_cast<double>(numerator) / denominator << '\n'; // prints 2.5

    return 0;
}

static_cast<double>(numerator) returns a temporary double object containing the converted value 20.0. This temporary serves as the left operand for floating-point division.

static_cast has two important properties.

First, static_cast provides compile-time type checking. If we attempt an impossible conversion, we get a compilation error.

    int value { static_cast<int>("Hello") }; // invalid: produces compilation error

Second, static_cast is (intentionally) less powerful than C-style casts, preventing certain dangerous conversions (like those requiring reinterpretation or discarding const).

Best Practice
Favor `static_cast` when converting between types.

Advanced note: Since static_cast uses direct initialization, any explicit constructors of the target class are considered when initializing the returned temporary object.

Using static_cast for Explicit Narrowing Conversions

Compilers often warn about potentially unsafe (narrowing) implicit type conversions. Consider:

int value { 65 };
char ch = value; // implicit narrowing conversion

Casting int (2 or 4 bytes) to char (1 byte) is potentially unsafe (the compiler can't determine if the integer overflows the char range), so compilers typically warn. With list initialization, the compiler would error.

To bypass this, use static cast to explicitly convert integer to char:

int value { 65 };

char ch { static_cast<char>(value) }; // explicit conversion from int to char

When we do this, we explicitly tell the compiler this conversion is intended, accepting responsibility for consequences (e.g., char overflow if it happens). Since this static cast output is type char, initializing variable ch creates no type mismatch, thus no warnings or errors.

Another example where compilers typically complain about double to int data loss:

int total { 100 };
total = total / 2.5;

To explicitly indicate intention:

int total { 100 };
total = static_cast<int>(total / 2.5);

We discuss more static_cast uses with class types in later lessons.

Casting vs Initializing a Temporary Object

To convert variable x to int, two conventional approaches exist:

  1. static_cast<int>(x), returning a temporary int object direct-initialized with x.
  2. int { x }, creating a temporary int object direct-list-initialized with x.

Avoid int ( x ), which is a C-style cast. This returns a temporary int direct-initialized with x's value (like expected from syntax), but has other C-style cast downsides (like potentially performing dangerous conversions).

Three notable differences exist between static_cast and the direct-list-initialized temporary:

  1. int { x } uses list initialization, disallowing narrowing conversions. Great for variable initialization where we rarely want data loss. But when casting, we presumably know what we're doing, and narrowing conversions should be allowed if desired. The narrowing restriction can impede this.

Example showing platform-specific issues:

#include <iostream>

int main()
{
    int numerator { 20 };
    int denominator { 8 };

    std::cout << double{numerator} / denominator << '\n'; // OK if int is 32-bit, narrowing if 64-bit
}

We decided to convert numerator to double for floating-point division instead of integer division. On 32-bit architecture, this works (a double can represent all 32-bit int values, so it isn't narrowing). But on 64-bit architecture, this is narrowing. Since list initialization disallows narrowing conversions, this won't compile on 64-bit architectures.

  1. static_cast makes conversion intent clearer. Though more verbose than the direct-list-initialized alternative, that's beneficial here, making conversions easier to spot and search for, ultimately making code safer and more understandable.

  2. Direct-list-initialization of a temporary only allows single-word type names. Due to a syntax quirk, several C++ places allow only single-word type names (the standard calls these "simple type specifiers"). While int { x } is valid conversion syntax, unsigned int { x } is not.

See this example producing a compile error:

#include <iostream>

int main()
{
    unsigned char letter { 'a' };
    std::cout << unsigned int { letter } << '\n';

    return 0;
}

Simple workarounds exist, easiest being a single-word type alias:

#include <iostream>

int main()
{
    unsigned char letter { 'a' };
    using UInt = unsigned int;
    std::cout << UInt { letter } << '\n';

    return 0;
}

But why bother when you can just static_cast?

For these reasons, we generally prefer static_cast over direct-list-initialization of a temporary.

Best Practice
Prefer `static_cast` over initializing a temporary object when conversion is desired.

Summary

Explicit type conversion (casting): Programmer-requested type conversion using cast operators, as opposed to implicit conversion performed automatically by the compiler.

C-style casts: Legacy casting syntax (type)value or type(value) that should be avoided. Can perform dangerous conversions, hard to search for, and unclear about which conversion is performed.

static_cast: The primary C++ cast for explicit type conversions. Provides compile-time type checking, is easy to search for, prevents dangerous conversions, and clearly indicates programmer intent.

Named casts: C++ cast operators (static_cast, dynamic_cast, const_cast, reinterpret_cast) that explicitly state their purpose and are type-safe.

Narrowing conversion safety: Use static_cast to explicitly mark intentional narrowing conversions, silencing compiler warnings and documenting that data loss is expected.

Temporary object initialization: While int{x} can convert x to int, static_cast<int>(x) is preferred for conversions because it's clearer, works with multi-word types, and doesn't disallow narrowing.

Understanding explicit type conversion and preferring static_cast helps you write clearer, safer code that makes conversion intent obvious and prevents accidental dangerous conversions.

Quiz

Question #1

What's the difference between implicit and explicit type conversion?

Show Solution

Implicit type conversion happens automatically whenever one data type is expected but a different one is supplied. Explicit type conversion happens when the programmer uses a cast to explicitly request conversion from one type to another.