Chapter 13 - Notes

Casting

Casting is a mechanism that allows the programmer to temporarily or permanently change the interpretation of an object by the compiler.

Note that this does not imply that the object is changed, but rather the interpretation thereof.

Operators that change the interpretation of an object are called casting operators.

Motivation

Consider the following real-world example: most modern C++ compilers support bool as a primitive type, though many libraries and programs were written a long time ago in C.

The C programming language did not support bool as a primitive type, so the programmers had to rely on the use of an integral type to hold boolean-like data:

typedef unsigned short boolean;

boolean isRaining(Day day) {
    if (...) {
        return 0;
    } else {
        return 1;
    }
}

If such code were to be used with a new application programmed and compiled using the latest C++ compiler, the programmer would have to find a way to convert such variables as a bool.

The way to make this happen is by using C-style casts:

bool raining = (bool)isRaining(today);

C++ introduces many new casting operators that behave differently. Some prefer the old C-style casts, while others prefer the newer operators introduced by C++.

Downsides of C-Style Casting

Type safety is an important aspect of the C++ language, rightfully disallowing code like this:

char* str = "ABCDEF";
int* intArray = str;

C-style casting however compromises this type safety by allowing anyone to force the compiler to interpret any variable as any type they wish:

char* str = "ABCDEF";
int* intArray = (int*)str;  // Really bad! 

C++ Casting Operators

Despite the disadvantages, the concept of casting is still quite valuable, and there are legitimate use cases for it.

C++ introduces four new casting operators specific to inheritance-based scenarios that did not exist in C:

  • static_cast

  • dynamic_cast

  • reinterpret_cast

  • const_cast

The usage of these casting operators is consistent to that of C-style casting:

destination_type variable_name = cast_operator<destination_type>(object_to_cast);

Static Cast

Static casting has two use cases:

  • Converting pointers between related types

  • Performing explicit type conversions for standard data types that would otherwise happen automatically or implicitly (recall conversion operators?)

static_cast implements a basic compile-time check to ensure that the pointer is being cast to a related type.

Using static_cast, a pointer of a derived can be upcasted to a pointer of its base type, or can be downcasted to a pointer of its derived type.

Base* base = new Derived();
Derived* derived = static_cast<Derived*>(base);  // OK!

Unrelated* unrelated = static_cast<Unrelated*>(base);  // NOT OK - compile error!

Casting a pointer of a derived class to a pointer of a base class is called upcasting. This can be done without the use of any casting operators.

Casting a pointer of a base class to a pointer of a derived class is called downcasting. This cannot be done without the explicit use of a casting operator.

static_cast can make implicit casts explicit:

double pi = 3.14159265;
int num = static_cast<int>(pi);

In the above example, the program would behave the same even without the casting operator. Its purpose here is merely to bring to the attention of other programmers that casting is occurring.

Such explicit casting is also necessary for objects who have defined conversion operators that have been marked as explicit.

Downsides

static_cast only verifies that the pointer types being casted to and fro are related. It does not perform any runtime checks.

The following bug will still compile:

Base* base = new Base();
Derived* derived = static_cast<Derived*>(base);

The derived variable contains a partially constructed Derived object, because the underlying object is of type Base. As such, any calls to derived member functions will result in undefined behaviour.

Dynamic Cast

Dynamic casting, as the name implies, is the opposite of static casting, and performs the cast at runtime as opposed to at compile time.

The result of the dynamic_cast operation can be used to determine whether the attempt at casting the object to a desired type has been successful.

Base* base = new Derived();
// Downcasting 
Derived* derived = dynamic_cast<Derived*>(base);

if (derived == nullptr) {
    std::cout << "Cast was unsuccessful" << std::endl;
} else {
    std::cout << "Cast was successful" << std::endl;
    derived->derived_function();
}

As shown above, the result of a dynamic cast if not successful will return a nullptr. The result returns a pointer to the desired type, as with static casting, if the cast was successful.

This is also known as Runtime Type Identification (RTTI) - which is to mean that the type of the underlying object held by a base type pointer can be identified at runtime.

Reinterpret Cast

reinterpret_cast is the closest C++ casting operator to a C-style cast. It allows the programmer to cast an object of any type into another, regardless of whether the two types are related.

It forces the reinterpretation of all the bytes of an object, as if it were of another type.

This is useful for very low-level programming interfaces, where a function returns an array of bytes (unsigned char*) that could represent an arbitrary data structure.

Reinterpret casting inherits all the benefits and downsides of C-style casting, and should generally be avoided unless completely necessary.

Const Cast

const_cast allows one to remove the const access modifier on an object.

If this sounds weird to you, then your understanding of const objects is well.

Consider the following example:

struct Box {
    // ...
    void display();  // This function is not marked const
};

void displayBox(const Box& box) {
    box.display();  // Compile error 
}

Now imagine that the source code for Box is elsewhere, and you have no control over it. In this situation the use of const_cast is helpful:

void displayBox(const Box& box) {
    Box& refBox = const_cast<Box&>(box);
    box.display();  // OK!
}

Though using const_cast to invoke non-const functions should only be used as a last resort. You should ensure that whatever function you intend to invoke actually does not mutate the state of the object.

Downsides of C++ Casting

Redundant

Most reasonable usage of static_cast is well covered by C-style casting, which has a much less verbose syntax:

double pi = 3.142;
int num = static_cast<int>(pi);  
// or 
int num = (int)pi;
// or
int num = pi;

In all cases, the same result is achieved, with num being 3.

Derived* derived = static_cast<Derived*>(base);
// or
Derived* derived = (Derived*)base;

In both cases, the same result is achieved. Though using static_cast has the extra benefit of compile-time checking.

reinterpret_cast and const_cast are both last resort measures that should never be used in the first place.

dynamic_cast is admittedly useful, but leads to poor program design, and has a large overhead each time it is invoked. It is generally avoidable in modern C++ applications.

Therefore, C++ casting operators are not so much better than C-style casting. Casting in general should be avoided; but when it is used, understand what happens behind the scenes.

Last updated

Was this helpful?