Published on

Understanding C++ Casts

C++, being a strongly typed language, is strict with its types. And there are always cases when you need to convert one type into another, which is known as casting.

int a = 10;
long b = a;

This is known as implicit conversion. More specifically, the above example is of standard conversion, which occurs automatically between fundamental types and few pointers.

There can also be implicit casts between classes with constructor or operator conversions.

struct A {};
struct B {
    B (A a) {}
}

A a;
B b = a;

Here an implicit conversion happened from A to B because B has a constructor that takes an object of A as parameter.

static_cast

static_cast can be used to convert between pointers to related classes (up or down the inheritance hierarchy). It can also perform implicit conversions.

class Mammal {};
class Human : public Mammal {};

Human* h = new Human;

Mammal* m = static_cast<Mammal *>(h);   // cast it to pointer to base type. static_cast here is unnecessary

Mammal* m2 = static_cast<Human *>(m);   // cast back to pointer to derived type

When we're casting up the hierarchy, static_cast is not needed. Every Human is a Mammal. So a Human * can be converted to a Mammal * implicitly. static_cast would actually perform this implicit cast if you use it anyway.

But every mammal may not be a Human. So implicit conversion from Mammal * to Human * is not allowed. That's where static_cast comes in. If we omit the static cast, we'll get an error something alone the line of

invalid conversion from 'Mammal*' to 'Human *'

But, using static_cast, we can still do the conversion anyway. static_cast doesn't perform any checks. So it's the programmer duty to ensure that the conversion should be valid. An invalid conversion might not fail, but can cause problems later when the pointer points to an incomplete type and is dereferenced.

class Mammal {};
class Human : public Mammal {
    public:
        virtual void scream () { // note the virtual
            cout << "MOM" << endl;
        }
}

Mammal* m = new Mammal;
Human* h = static_cast<Human *>(m); // OK so far
h->scream();    // Mayhem!!

So don't use static_cast to cast down the hierarchy unless you're absolutely sure the conversion is valid.

static_cast won't let you convert between two unrelated classes

class A {};
class B {};
A* a = new A;
B* b = static_cast<A *>(a);

Gives an error -

cannot convert 'A*' to 'B*' in initialization

dynamic_cast

dynamic_cast is related to static_cast in the sense that it helps to cast through inheritance, but it's more powerful than static_cast but has an overhead. It performs a check in order to prevent the case above.

class Mammal {};
class Human : public Mammal {};

Human* h = new Human;
Mammal* m = dynamic_cast<Mammal*>(h);   // ok
Human* h1 = dynamic_cast<Human*>(m);    // error

The second conversion would produce a compilation error since base-to-derived conversion are not allowed with dynamic_cast unless the base class is polymorphic (has at least one virtual function, either declared or through inheritance).

class Mammal {
    public:
        virtual void scream(){}
};

class Human : public Mammal {
    public:
        void scream() override {
            cout << "MOM" << endl;
        }
}

Human* h = new Human;
Mammal* m = dynamic_cast<Mammal *>(h);
Human* h1 = dynamic_cast<Human *>(m);
h1->scream();

This works as you'd expect.

What happens if we try to cast a Mammal * to a Human * where the Mammal is not actually a Human? (that crashed the static_cast one)

Mammal* m = new Mammal;
Human* h = dynamic_cast<Human *>(m);

if(h == nullptr) {
    cout << "Oops! Cast failed!" << endl;   // this will be the result
} else {
    h->scream();
}

So, as you can see, dynamic_cast performs a check. It returns nullptr if you're trying to convert pointers or throws std::bad_cast if you're trying to convert references.

But remember, this check happens in runtime, and not compile time. It require the Run-Time Type Information (RTTI) to keep track of dynamic types and thus has a slight overhead.

Let's see now what happens when we try to convert two unrelated classes

class A {
    public:
        void f(){}
};

class B {};

A* a = new A;
B* b = dynamic_cast<A *>(a);

This doesn't give a compilation error (unlike static_cast, because the check is performed at run time at that point b will be nullptr)

Finally, one more thing dynamic_cast can do is "side cast." To understand this, consider this classic "dreaded diamond" hierarchy

struct A {
    virtual void f() = 0;
}

struct L : A {
    virtual void f() override {
        cout << "Left" << endl;
    }
}

struct R : A {
    virtual void f() override {
        cout << "Right" << endl;
    }
}

struct D : L, R {

}

By side casting, we mean to say that we should be able to cast an object of type L as type R and it should behave exactly as type R (and vice versa). This is of course possible only when the underlying object is actually of type D. static_cast however, can't help here.

D* d = new D;
L* l = d;
R* r= d;
A* bl = l;
A* br = r;
br->f();    // Right

static_cast<L *>(br) -> f(); // still prints "Right"
dynamic_cast<L *>(br) -> f(); // prints "Left"bl -> f(); // prints "Left";
static_cast<R *>(bl) -> f(); // still prints "Left"
dynamic_cast<R *>(bl) -> f(); // prints "Right"

const_cast

const_cast is the only cast that can be used to add const to a type or take const out of a type. This is useful when, say you want to pass a non const argument to a function which expects const arguments.

void f(int& x) {
    x = 5;
}

It expects a non const reference to int.

Now suppose you have something like this

int i = 4;  // non const variable
const int& j = i;   // const reference

If you try to call f with j as argument, you'll get an error

error: binding reference of type 'int&' to 'const int' discards qualifiers

What you can do, is to remove the const with const_cast

f(const_cast<int&>(j));

Note however, that you can remove const away from an object only if it was actually declared as non const. Removing const from a const object is undefined behavior

const int i = 3;
int* p = const_cast<int*>(&i);
*p = 4; // undefined behavior

Similarly, you can also add const to an object. const_cast works on volatile keyword too, although that's rare.

reinterpret_cast

This cast converts any type of pointer to any other type of pointer, even unrelated types. No checks are performed. It simply copies the binary data from one pointer to another.

All types of pointer conversions are allowed. You can even cast pointers to and from integer types. The only guarantee is that if you cast a pointer to an integer type that is large enough to hold it back, and then cast it back to pointers, you get a valid pointer

References: https://codeburst.io/understanding-c-casts-ef1f36e54240