Han Solo’s Guide to C++ Casting

Clear your C++ cast concepts with Star Wars

P.G. Baumstarck
Better Programming

--

It’s all so simple.

C++ casts are a common stumbling block for developers. What’s the difference between a static_cast and a dynamic_cast? Or a C-style cast versus std::move? Under what circumstances do you even need to use a const_cast?

Luckily Han Solo provides a handy rubric to disentangle the many threads of C++ casting.

Base Class

We start with a base class, which will be represented by Han Solo as portrayed by Harrison Ford in the original 1977 Star Wars:

class HanSolo {};

class HarrisonFord : public HanSolo {
private:
int actor_;
};

HanSolo* han_solo = new HarrisonFord();

So far so good.

static_cast

The most straightforward cast is static_cast, which is represented by Han Solo as reprised by Harrison Ford in the 2015 film Star Wars: The Force Awakens.

He’s the same actor, just more experienced, so it’s safe to perform a cast between these two Han Solos that are the same Harrison Ford underneath (though it might cause undefined behavior if we try to replace the older, more-derived 2015 Harrison Ford with the younger 1977 Harrison Ford):

class HanSolo {};

class HarrisonFord1977 : public HanSolo {
private:
int a_new_hope_;
};

class HarrisonFord2015 : public HarrisonFord1977 {
private:
int the_force_awakens_;
};

HanSolo* young_han = new HarrisonFord1977();
HanSolo* old_han = new HarrisonFord2015();

// This compiles but produces undefined behavior.
HarrisonFord2015* undefined_han = static_cast<HarrisonFord2015*>(young_han);
// This compiles and is safe.
HarrisonFord2015* old_han1 = static_cast<HarrisonFord2015*>(old_han);

This is static_cast. It only performs compile time type-checking to make sure that the two classes involved in the cast are related. It allows downcasting to a more derived type, and upcasting to a less derived type.

This does allow unsafe conversions because it will downcast a base class into a derived class without checking the type of the pointer.

dynamic_cast

A step up from static_cast is dynamic_cast, which is represented by Han Solo as portrayed by Alden Ehrenreich in the 2018 film Solo: A Star Wars Story. He’s a completely different actor in a spin-off series, so it’s not safe to just swap in one Han Solo for another.

We need to perform a runtime type check to make sure that our two Han Solos are compatible — even though they appear to be “implementing the same interface:”

class HanSolo {};

class HarrisonFord1977 : public HanSolo {
private:
int a_new_hope_;
};

class AldenEhrenreich2018 : public HanSolo {
private:
int a_star_wars_story_;
};

HanSolo* harrison = new HarrisonFord1977();
HanSolo* alden = new AldenEhrenreich2018();

// This compiles and produces a valid pointer.
HarrisonFord1977* young_han1 = dynamic_cast<HarrisonFord1977*>(harrison);
// This compiles but returns `nullptr` instead.
HarrisonFord1977* young_han2 = dynamic_cast<HarrisonFord1977*>(alden);

This is dynamic_cast. It performs extra runtime type-checking of the pointers involved to make sure that it doesn’t perform an unsafe downcast, which would otherwise be permitted by static_cast.

Using this we can make sure not to accidentally cast an Alden Ehrenreich to a Harrison Ford.

const_cast

Another peculiarity of C++ is the const_cast, which is represented by Han Solo frozen in carbonite from 1980’s Star Wars V: The Empire Strikes Back. Han is still Han, but he’s been frozen, so we require a const_cast to thaw him back out to his mutable state:

class HanSolo {};

class HarrisonFord : public HanSolo {
public:
int original_trilogy;

void check_trilogy() const {
// This line wouldn't compile since the method is `const`.
// original_trilogy += 3;
}

void break_trilogy() const {
HarrisonFord* unfrozen_hans = const_cast<HarrisonFord*>(this);
unfrozen_hans->original_trilogy += 3;
}
};

const HarrisonFord* carbonite_han_solo = new HarrisonFord();
carbonite_han_solo->break_trilogy();

This is const_cast. The const keyword was added to C++ to enable more compiler optimizations and faster performance. const code paths can generally be more efficient than mutable code paths, and the compiler should be able to opt-in to using these whenever possible.

But, since this is mostly an element of optimization and not of type safety, const_cast allows you to strip out the safety checks and trigger potentially undefined behavior whenever you like.

reinterpret_cast

The epitome of casting is reinterpret_cast, which is represented by Jabba the Hutt from 1983’s Star Wars VI: Return of the Jedi. Because Jabba the Hutt is absolutely not Han Solo in any way — but you can still cast him to one using reinterpret_cast:

class HanSolo {};

class JabbaTheHutt {};

HanSolo* han1 = new HanSolo();
JabbaTheHutt* jabba1 = new JabbaTheHutt();

// All totally fine.
HanSolo* han2 = reinterpret_cast<HanSolo*>(jabba1);
JabbaTheHutt* jabba2 = reinterpret_cast<JabbaTheHutt*>(han1);

This is reinterpret_cast. It tells the compiler to simply throw away one type and replace it with another, without any checks whatsoever. Maybe you need this because you know the behavior you’re seeking is correct, and this really is one of those “Break glass in case of emergency”–level emergencies, so you go ahead and reinterpret_cast one thing to another.

But, if the bytes don’t exactly align the way they should, this will lead to undefined behavior.

std::move

A novel addition to C++ as of C++11 (and revised in C++14) is std::move, which is represented by Han Solo tied to a spit and being transported by Ewoks in 1983's Star Wars VI: Return of the Jedi. Here Han Solo is the same actor in the same epoch, he’s just being transported somewhere against his will:

std::string han_solo = "Han Solo";
std::cout << "It's " << han_solo << std::endl;
// Prints: "It's Han Solo"

std::vector<std::string> ewoks;
ewoks.push_back(han_solo);
std::cout << "It's still " << han_solo << std::endl;
// Prints: "It's still Han Solo"

ewoks.push_back(std::move(han_solo));
std::cout << "He's gone! " << han_solo << std::endl;
// Prints: "He's gone! "

This is std::move. When we have an object in one location that we want to replace the memory in a different location, std::move will facilitate the transfer without invoking any extraneous copies along the way.

While this is all about efficiency, it can produce unexpected behavior if programmers are not used to its sometimes non-obvious side effects.

Summary

When you first start studying casts in C++, it may seem that there are so many of them with slightly different names, with highly nuanced functions, and dubious safety.

But, once you get a little more experience, you realize that — all that is indeed true, it’s just that sometimes unsafe and undefined behavior is exactly what it takes to get your code compiling.

--

--

Silicon Valley software engineer. My opinions are my own and not necessarily those of my employer.