Introduction

This blog is about the four type conversions in C++: static_cast, const_cast, reinterpret_cast, const_cast, their usage and some usage scenarios.

static_cast

The reference scenario of static_cast compares conversions, according to CppReference, to perform static type conversions from the expression e to T in the following cases.

Common types

  1. for the expression e can be converted from an implicit type to T
  2. If a standard conversion sequence exists for types from T to e, a static type conversion can perform the inverse of that implicit type conversion sequence. This conversion does not include: left-value to right-value, array to pointer, function to pointer conversions
  3. if the conversion from e to T involves left-to-right, array-to-pointer, function-to-pointer conversions, this can be done explicitly via static type conversion
  4. if T is of type void, the static type conversion will discard the value of the expression e

Some examples are shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <string>

class Base {
public:
    Base(int a) {}
};

int main() {
    // static_cast does a implicit conversion -- 1
    int a = 10;
    Base b = static_cast<Base>(a + 1);

    // static_cast does a inverse of standard conversions through qualification check -- 2
    std::string str = "str";
    char* c = &str[0];
    const char* const* d = &c;
    const char* const* const e = static_cast<const char* const* const>(d);

    // static_cast does a array-to-pointer -- 3
    int f[5] = {1, 2, 3, 4, 5};
    int* g = static_cast<int*>(f);

    // discard value of a only for suppressing unused warning -- 4
    int h;
    static_cast<void>(h);

    return 0;
}
  • Enumeration types
    1. scoped enumerations can be converted to integer or floating point types
    2. the value of an enum or integer can be converted to any full enum type

Some examples are shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

enum class A { aa = 1, bb, cc };

enum B { ee /* = 0*/, ff };

enum C { gg, hh };

int main() {
    // static_cast does a scoped enum to int/double conversion
    A a = A::aa;
    int b = static_cast<int>(a);
    double c = static_cast<double>(a);
    std::cout << b << ", " << c << std::endl;  // 1, 1

    // static_cast does a enum/int conversion
    int d = 0;
    B e = static_cast<B>(d);  // 0 -> B::ee
    C f = static_cast<C>(e);  // B::ee -> C::gg

    return 0;
}
  • Reference types
    1. if type T is a right-valued reference type and e is an object, a static type conversion converts it to a would-be value of the same type, for example std::move.
  • (unsafe) Pointer types containing conversions between inheritance relations
    1. if a reference or pointer of type T and e is a left-valued or purely right-valued pointer to its base class B, a down-conversion (base class to derived class) is possible, no guarantee that there is really a derived class variable in that memory location
    2. a pointer to a member variable of derived class D can be upconverted to a pointer to its base class B without guaranteeing the existence of the member in the base class
    3. a pointer to void can be converted to a pointer to any object type

Some examples of unsafe use are shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
class Base {};

class Derived : public Base {
public:
    int a = 1.0;
};

int main() {
    Base a;
    // static_cast does a down casting in wrong situation
    Base* b = &a;
    Derived* c = static_cast<Derived*>(b);
    Derived& d = static_cast<Derived&>(a);

    // will run, but is random value
    std::cout << d.a << std::endl;

    // static_cast can cast a void* to pointer of any type
    void* e = static_cast<void*>(c);
    Derived* f = static_cast<Derived*>(e);

    // upcast of a pointer to member
    int Derived::*pa = &Derived::a;
    a.*static_cast<int Base::*>(pa);
    return 0;
}

As you can see that conversions involving inheritance relationships between static types are not safe because static types are only compile-time checked and may lead to unclear behavior at runtime if they pass compile-time checking. Therefore for this type of conversion it is better to use dynamic type conversion.

dynamic_cast

dynamic_cast can implement upward, downward or sideways conversions of pointers or references on inheritance relationships. According to CppReference, there are the following cases where a dynamic type conversion can be performed.

  1. if the expression e and T are of identical type, or T is more constant than e, a dynamic type conversion of e to T can be performed (static_cast is also possible)
  2. if the expression e is a zero pointer value ( nullptr ), you can convert to a zero pointer of T
  3. if T is a pointer or reference to a base class, the expression e is a pointer and reference to a derived class and the base class is unique. Dynamic type conversion converts the base class component of the derived class object to a pointer to the base class object (implicit type conversion and static_cast can do the same)
  4. if the expression e is polymorphic and T is a pointer to void, the dynamic type conversion converts it to the most derived pointer
  5. If the expression e is a pointer or reference to a polymorphic base class and T is a pointer to a derived class, the dynamic type conversion will perform a runtime check:
    1. check the derived hierarchy of the object referred to by e, if the type T can be obtained from e, then the conversion can be performed successfully (downcast)
    2. if e itself is not of a type at one level of the derived hierarchy, but the object it refers to is the most derived type, then it can be converted to T (sidecast, see example below)
  6. If dynamic type conversion is used directly or indirectly in a constructor or destructor, if e is the object this being constructed, then the object is considered to be the most derived object, and the behavior is undefined if T is not the type of the object itself or its base class.

The following are some examples.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <iostream>
class Base {
public:
    virtual void func() {}
};

class MiddleA : public virtual Base {};

class MiddleB : public virtual Base {};

/**
 *       Base
 *      /    \
 *     /      \
 * MiddleA  MiddleB
 *    \       /
 *     \     /
 *     Derived
 */
class CrossDerived : public MiddleA, public MiddleB {};

class Derived : public Base {};

int main() {
    CrossDerived a;
    MiddleB& b = a;

    // down cast
    CrossDerived& c = dynamic_cast<CrossDerived&>(b);
    // side cast
    MiddleA& d = dynamic_cast<MiddleA&>(b);

    // most common use case
    Base* e = new Derived();
    Derived* f = dynamic_cast<Derived*>(e);

    return 0;
}

dynamic_cast is mainly used to convert a dynamic base class pointer to a derived class pointer. Since diamond inheritance is rarely used in projects, side-cast cases are rare.

reinterpret_cast

reinterpret_cast can perform conversions between any pointer variables, and is generally used less often. The scenario can be used when there is some per-bit information flow, when packing and decompressing data. As the following example shows, we convert a data type to a generic data type with reinterpret_cast, and then somehow get the data and reconvert it to the data type we need.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>

struct Data {
    char a;  // 1 bytes
    char b;  // 1 bytes
    char c;  // 1 bytes
    char d;  // 1 bytes
};

struct PackedData {
    int data;  // 4 bytes
};

int main() {
    // intialized data
    Data d;

    // packed it and send it for some data stream
    PackedData* pd = reinterpret_cast<PackedData*>(&d);

    // data assign, memory layout for pd->data would be '61 62 63 64' or '64 63 62 61' depending on machine
    pd->data = ('a' << 3 * 8) | ('b' << 2 * 8) | ('c' << 1 * 8) | 'd';
    std::cout << pd->data << std::endl;

    // optional, changes have been made on d
    Data* d2 = reinterpret_cast<Data*>(pd);
    std::cout << d2->a << d2->b << d2->c << d2->d << std::endl;

    return 0;
}

const_cast

const_cast can be used to remove constancy from a type and is not recommended in general.

1
2
3
4
5
6
7
8
#include <iostream>
 
int main() 
{
    int i = 3;                 // i is not declared const
    const int& rci = i; 
    const_cast<int&>(rci) = 4; // OK: modifies i
}