Type Casting

Learned in CS247.

Casting allows us to take one type and treat it as another.

In C:

Node n;
int* ip = (int*) &n; // (int*) is the casting operator - treat this as an int*

This is dangerous, generally avoid casting unless necessary, subverts type system.

If necessary, use one of the 4 C++ casts instead of C-style casting:

  1. static_cast - “well-defined” conversions between two types
  2. reinterpret_cast - allows for poorly defined implementation-dependent casts
  3. const_cast - “remove” constness
  4. dynamic_cast - used for safely casting between pointers/references in an inheritance hierarchy

1) static_cast

static_cast allows for “well-defined” conversions between two types.

void g(float f);
void g(int n);
float f;
g(static_cast<int>(f));
  • g calls int version of g

Another example:

Book* b = ...;
Text* t = static_cast<Text*>(b);
  • “trust me bro, I know it’s a Text”
  • Undefined behavior if it’s not actually pointing at a Text

What counts as "well-defined"?

Well-defined conversions in C++ using static_cast include:

  1. Conversion between numeric types (e.g., from int to float).
  2. Conversion from pointer-to-base to pointer-to-derived (upcasting).
  3. Conversion between explicitly convertible types (using conversion constructors or operator overloads).
  4. Conversion from Void Pointers to any pointer type.
  5. Conversion between Enum (C++)s and Integral Types.

I asked for a counter example that clearly illustrates that static_cast is better than the c-style casting

const int ci = 10;
int* pi = (int*)&ci; // C-style cast removes constness - unsafe
// int* pi = static_cast<int*>(&ci); // Error: static_cast cannot cast away constness
 
void* pv = pi;
float* pf = (float*)pv; // C-style cast reinterprets the bit pattern - unsafe
// float* pf = static_cast<float*>(pv); // Error: static_cast cannot change type completely

2) reinterpret_cast

reinterpret_cast allows for poorly defined implementation dependent casts. Most uses of reinterpret_cast are undefined behavior.

Rational r;
Node* n = reinterpret_cast<Node*>(&r);

Why use reinterpret_cast?

Rarely a use case for it, you should probably use static_cast. StackOverflow.

”reinterpret_cast only guarantees that if you cast a pointer to a different type, and then reinterpret_cast it back to the original type, you get the original value.”

Example from Ross Evans code:

#include <iostream>
using namespace std;
 
class C {
  int x;   // private
 public:
  explicit C(int xvalue): x(xvalue) {}
 
  int getX() const { return x; }
};
 
class RogueC {
 public:
  int x;
};
 
int main() {
  C c(10);
  cout << c.getX() << endl;
  RogueC *r = reinterpret_cast<RogueC*>(&c);
  r->x = 20;
  cout << c.getX() << endl;
}
 
/* OUTPUT
10
20
*/

3) const_cast

const_cast is the only type of cast that can “remove” constness.

Example: using some library that gives us:

void g(int* p);

Let’s say we know g doesn’t modify the int pointed to by p in any way. Also, I have a const int* I’d like to call g with. Compiler will prevent us from calling g, because it might modify p.

I can use const_cast to call g in the following way:

void f(const int* p){
	g(const_cast<int*> p);
}

Generally, const_cast should be avoided.

Another example, working on legacy codebase that doesn’t use const anywhere. We want to add consts, make our program const-correct.

Why use const_cast?

The main problem that const_cast fixes is const-poisoning: adding const in one location often means we must add it to other locations to allow it to continue to compile.

We can use const_cast to bridge between const-correct and non-const-correct parts of our program. Make small independent parts of the program const-correct, use const-cast to allow the program to compile as the work is done.

What would happen if g actually modifies p? Then, it will end up modifying p. The compiler will not complain.

Important

the type in a const_cast must be a pointer, reference, or pointer to member to an object typeC/C++(717).

4) dynamic_cast

dynamic_cast is used for safely casting between pointers/references in an inheritance hierarchy

Book* pb = ...;
static_cast<Text*>(pb)->getTopic();

Only safe if pb actually points to a Text.

Instead,

Book* pb = ...;
Text* pt = dynamic_cast<Text*>(pb);
  • If the cast succeeds (i.e. dynamic type is Text), then pt points at the Text. Otherwise, pt is set to nullptr.
if (pt) cout << pt->getTopic();
else cout << "Not a Text!";

Also can be used on references:

Text t{...};
Book& br = t;
Text& tr = dynamic_cast<Text&>(br);
  • What if br is actually referencing a Comic? Cannot set it to null, since there is no such thing as a null reference.
  • If dynamic_cast fails with a reference: throw a std::bad_cast exception.

When to use dynamic_cast?

More often than not, dynamic_cast is WRONG. You should rather be trying to leverage Polymorphism (more below). StackOverflow

static_cast vs dynamic_cast:

  • The use of dynamic_cast is safer as it performs a runtime check to ensure that the downcast is valid, returning a null pointer if the downcast fails. With static_cast, there is no runtime check, so improper downcasting may lead to undefined behavior. Therefore, care must be taken when downcasting, especially when using static_cast.

Virtual Method

dynamic_cast only works if you have at least one virtual method. Why? To enable Dynamic Dispatching, so a vptr and vtable is created, and thus help us determine the dynamic type of the object.

Smart Pointer version of Casts

There exist smart pointer versions of each of these casts:

static_pointer_cast
const_pointer_cast
dynamic_pointer_cast

Cast shared_ptrs to other shared_ptrs.

Those allow us to make decisions based on Run-Time Type Information (RTTI):

void whatIsIt(shared_ptr<Book> b) {
	if (dynamic_pointer_cast<Text>(b)) cout << "Text";
	else if (dynamic_pointer_cast<Comic>(b)) cout << "Comic";
	else cout << "Normal Book";
}

BAD

The above function is poor Object-Oriented Design (because violates polymorphism and Open-Closed Principle). Either use simple Polymorphism within derived classes with Function Overriding, or use the Visitor Pattern if you want to write a method that depends on Polymorphic Types.

Something like this is better

class Animal {
    virtual void Process() = 0;
}
 
class Cat : public Animal {
    void Process() { std::cout << " I am a tiny cat"; }
}
 
class Bear : public Animal {
    void Process() { std::cout << "I am a big bear"; }
}
 
void func(Animal* animal) {
    if (animal != nullptr) { animal->Process(); }
}

Recall: polymorphic assignment problem. We considered making operator= virtual.

  • operator= non-virtual: partial assignment
  • operator= virtual: mixed assignment
Text t1, t2;
Book& b1 = t1;
Book& b2 = t2;
b1 = b2;

Let’s make operator= virtual in Book.

Text& Text::operator=(const Book& other) {
	// line below throws if `other` is not a Text
	Text& tother = dynamic_cast<const Text&>(other);
	if (this == &tother) return *this;
	Book::operator=(other);
	topic = tother.topic;
	return *this;
}