Casts

With our Rational class, we saw how we can convert integers to Rational implicitly. What about making a conversion that goes the other way? Can we make Rational convert to something like a double?

Well yes, yes we can.

    // in the Rational class
    operator double() const { return num / static_cast<double>(den); }

This conversion is implicit. Which can lead to a problem:

Rational r(10, 2);
if (r == 10) {
    // do we promote 10 to Rational and call operator==(Rational, Rational)
    // or de we convert r to a double and do operator==(double, int)?
}

Implicit conversions (either constructor or conversion operator) are not really the best practice. Especially when that conversion is to/from integral types. Consider some things that these implicit conversions allow, but don't make much semantic sense:

Rational r(10, 20);
if (r == 'h') {
    // convert 'h' to an integer, then to a Rational
    // or convert r to double and compare with integral value of 'h'
}

r = true; 
//convert true to int and int to Rational, then update via operator=

Now with a Rational, most things make sense except chars and bools. But, just like we can make the conversion constructor explicit, we can make the conversion operator explicit.

    explicit operator double() const { return num / static_cast<double>(den); }

Now we can use static_cast<double> to explicitly convert a Rational.

Rational r(10);

const auto rd = static_cast<double>(r);

static_cast<T>(E) is mostly safe and only permitted if there is a valid conversion from the type of E to T. Such a conversion may be implicit or explicit. static_cast cannot cast away qualifiers such as const. Moreover static_cast can perform upcasts (casting a Derived class reference to a base class reference) and downcasts (casting a base class reference to a derived class reference) provided the cast is unambiguous (no duplication due to diamond hierarchies, multiple inheritance, or virtual inheritance). However static_cast performs no checks that the Base class's actual type is the Derived class during a downcast.

const auto i = static_cast<int>(54.078);
const auto d = static_cast<float>(true);

class B { 
public:
    virtual ~B() = default; 
};
class D : public B {};

D d;
B & b = d;

auto d2 = static_cast<D&>(b);
// notice that we are casting to a reference to D and not to D itself
// dangerous

auto b2 = static_cast<B&>(d);
// upcast, safe

For a safer way to downcast, C++ provides dynamic_cast<T>(E). Like static casts, dynamic_cast can perform upcasts and adding const. But where it differs greatly is during downcasts. dynamic_cast only works on polymorphic hierarchies (those which have at least one virtual function). Given dynamic_cast<T>(E), if T is a pointer or reference to a type derived from the static type of E, and the dynamic type of E is T or a derived type of T, the cast succeeds. Otherwise, the cast fails and throws std::bad_cast if used on references or returns nullptr if used on pointers. dynamic_cast works with virtual inheritance, unlike static_cast, and costs an extra type check at runtime. Furthermore, the cast returns nullptr if E evaluates to nullptr.

class B { 
public:
    virtual ~B() = default; 
};
class D : public B {};
class C : public D {};

D d;
B & b = d;
C c;

auto d2 = dynamic_cast<D&>(b);
// notice that we are casting to a reference to D and not to D itself
// now safe, will perform a runtime type check

auto b2 = dynamic_cast<B&>(d);
// upcasts incur no runtime check


auto c2 = dynamic_cast<C&>(b); 
// std::bad_cast
// undefined behavior if this were static_cast

Dangerous Casts

First up on the "bad boy" side of town is const_cast<T>(E). This one allows you to strip qualifiers off the type of E.

const int a = 10;
auto t = static_cast<int>(a); //error, a is const

auto t2 = const_cast<int>(a); //works

auto t3 = const_cast<const int>(t2);

It also allows you to add qualifiers as well.

reinterpret_cast<T>(E) completely reinterprets the bytes of E as the bytes of T.

double d = 58.01;
auto d2 = *reinterpret_cast<long long*>(&d);
// d2 is not 58
// instead, it will read the floating point representation of d
// and interpret that as twos-complement

int num = 100;
auto bytes = reinterpret_cast<std::byte*>(&num);
bytes[0]; 
// most likely 100 for little endian, 0 for big endian

const double dub = 10;
auto bytes = reinterpret_cast<std::byte*>(&dub);
// error, cannot cast away const

Finally, there is the C-style cast. This guy should be avoided because you will never know what type of cast is actually being performed. The C-style cast has the most power and can pretty much convert anything to anything else. An example problem: You want to use a conversion function but forgot to implement it. A C-style cast won't complain and will reinterpret the bytes which is not what was intended! A static_cast would fail to compile.

Possible Exercises

  1. Add explicit conversions to/from std::vector for the Vec3D class exercise.