Lei Mao bio photo

Lei Mao

Machine Learning, Artificial Intelligence, Computer Science.

Twitter Facebook LinkedIn GitHub   G. Scholar E-Mail RSS

Introduction

In C++ programming, it is not recommended to use the C style casting because C style casting often has ambiguous meaning to the programmer. There are four common types of casting in C++, static_cast, const_cast, dynamic_cast, and reinterpret_cast.


In this blog post, I would like to discuss some of the basic usages of the C++ casts by giving some concrete examples.

Static Cast

static_cast is commonly used to cast between numerical types and covert void* to pointers pointing to a variable of a certain type. The user should pay attention to the range of the numerical type. For example, casting a valid large long typed variable to uint8_t typed variable would not give you the desired result since uint8_t could not hold large values.


In addition, static_cast checks type conversion compatibility at compile-time, but there is no runtime check. So it could be dangerous for some pointer type conversions.

/*
 * static_cast.cpp
 */
#include <iostream>
#include <cstdint>

int main()
{
    int vInt = 65;
    float vFloat = 1.2;
    char vChar = 'A';

    uint8_t vUInt8 = 128;
    long vLong = 123456789;

    // Cast numerical types
    std::cout << static_cast<char>(vInt) << std::endl; // 65 -> 'A'
    std::cout << static_cast<int>(vChar) << std::endl; // 'A' -> 65
    std::cout << vUInt8 / 255 << std::endl; // 0
    std::cout << static_cast<float>(vUInt8) / 255 << std::endl; // 0.501961
    std::cout << static_cast<uint8_t>(vLong) << std::endl; // This is bad. Know what you are doing!

    size_t nbBytes = 12;
    void* pBuffer = malloc(nbBytes);
    // Cast void* to T*
    int* pIntBuffer = static_cast<int*>(pBuffer);
    size_t numInts = nbBytes / sizeof(int);
    for (int i = 0; i < numInts; i ++)
    {
        pIntBuffer[i] = i;
    }
    for (int i = 0; i < numInts; i ++)
    {
        std::cout << pIntBuffer[i] << " ";
    }
    std::cout << std::endl;

    free(pBuffer);
}

To compile the program, please run the following command in the terminal.

$ g++ static_cast.cpp -o static_cast --std=c++11

The expected output of the program would be as follows.

$ ./static_cast 
A
65
0
0.501961

0 1 2 

Const Cast

const_cast is commonly used to cast away the const specifier for any const typed pointers. With the const specifier, the user is not allowed to modify the value of the variable which the pointer points to via dereferencing the pointer. Once the const specifier for the pointer is cast away by const_cast, we could modify the value of the variable via dereferencing the pointer, as long as the variable is modifiable.

/*
 * const_cast.cpp
 */
#include <iostream>

int main()
{
    int* pIntConst = nullptr;

    int vInt = 1;
    const int* pConstInt = &vInt;
    std::cout << vInt << std::endl;
    // *pConstIntConst = 2; // Not allowed
    // Cast away const here will be useful
    pIntConst = const_cast<int*>(pConstInt);
    *pIntConst = 2;
    std::cout << vInt << std::endl;

    std::cout << "-------------------" << std::endl;

    // Non-modifiable int
    const int vIntConst = 1;
    const int* pConstIntConst = &vIntConst;
    // pIntConst = pConstIntConst; // Not allowed
    std::cout << vIntConst << std::endl;
    // Cast away const here will not be useful
    pIntConst = const_cast<int*>(pConstIntConst);
    // Still cannot change the value for a const int variable
    *pIntConst = 2;
    std::cout << vIntConst << std::endl;
}

To compile the program, please run the following command in the terminal.

$ g++ const_cast.cpp -o const_cast --std=c++11

The expected output of the program would be as follows.

$ ./const_cast 
1
2
-------------------
1
1

Dynamic Cast

dynamic_cast is commonly used to cast between the pointers to the classes up, down, and sideways along the inheritance hierarchy. It does runtime check. If it turns out to be an invalid cast after runtime check, a nullptr would be returned.

/*
 * dynamic_cast.cpp
 */
#include <iostream>

class A
{
public:
    virtual void func_virtual()
    {
        std::cout << "Virtual method from class A" << std::endl;
    }
    void func_non_virtual()
    {
        std::cout << "Non-virtual method from class A" << std::endl;
    }

};

class B: public A
{
public:
    virtual void func_virtual()
    {
        std::cout << "Virtual method from class B" << std::endl;
    }
    void func_non_virtual()
    {
        std::cout << "Non-virtual method from class B" << std::endl;
    }

};

class C: public A
{
public:
    virtual void func_virtual()
    {
        std::cout << "Virtual method from class C" << std::endl;
    }
    void func_non_virtual()
    {
        std::cout << "Non-virtual method from class C" << std::endl;
    }

};

class D: public B, public C
{
public:
    virtual void func_virtual()
    {
        std::cout << "Virtual method from class D" << std::endl;
    }
    void func_non_virtual()
    {
        std::cout << "Non-virtual method from class D" << std::endl;
    }
};

int main() 
{
    A a;
    B b;
    C c;
    D d;

    A * p_a = nullptr;
    B * p_b = nullptr;
    C * p_c = nullptr;
    D * p_d = nullptr;

    p_a = &a;
    p_b = &b;
    p_c = &c;
    p_d = &d;

    // Up cast
    // C++ runtime polymorphism
    // Always successful
    (dynamic_cast<A *> (p_b))->func_virtual(); // B
    (dynamic_cast<A *> (p_b))->func_non_virtual(); // A
    (dynamic_cast<A *> (p_c))->func_virtual(); // C
    (dynamic_cast<A *> (p_c))->func_non_virtual(); // A

    (dynamic_cast<B *> (p_d))->func_virtual(); // D
    (dynamic_cast<B *> (p_d))->func_non_virtual(); // B

    (dynamic_cast<C *> (p_d))->func_virtual(); // D
    (dynamic_cast<C *> (p_d))->func_non_virtual(); // C

    p_a = &a;
    p_b = &b;
    p_c = &c;
    p_d = &d;

    // Down cast
    // These would fail
    p_b = dynamic_cast<B *> (p_a);
    if (p_b == nullptr)
    {
        std::cout << "Unsuccessful casting A -> B" << std::endl;
    }
    else
    {
        std::cout << "Successful casting A -> B" << std::endl;
    }
    p_c = dynamic_cast<C *> (p_b);
    if (p_c == nullptr)
    {
        std::cout << "Unsuccessful casting B -> C" << std::endl;
    }
    else
    {
        std::cout << "Successful casting B -> C" << std::endl;
    }

    p_a = &a;
    p_b = &b;
    p_c = &c;
    p_d = &d;

    // These would work
    // First up cast
    p_a = dynamic_cast<A *> (p_b);
    // Then down cast
    p_b = dynamic_cast<B *> (p_a);
    if (p_b == nullptr)
    {
        std::cout << "Unsuccessful casting B -> A" << std::endl;
    }
    else
    {
        std::cout << "Successful casting A -> B" << std::endl;
    }
} 

To compile the program, please run the following command in the terminal.

$ g++ dynamic_cast.cpp -o dynamic_cast --std=c++11

The expected output of the program would be as follows.

$ ./dynamic_cast 
Virtual method from class B
Non-virtual method from class A
Virtual method from class C
Non-virtual method from class A
Virtual method from class D
Non-virtual method from class B
Virtual method from class D
Non-virtual method from class C
Unsuccessful casting A -> B
Unsuccessful casting B -> C
Successful casting A -> B

Reinterpret Cast

reinterpret_cast is commonly used for pointer cast. It asks the compiler to treat the expression as if it has the new type. It would not check anything so it is the most dangerous cast.

/*
 * reinterpret_cast.cpp
 */
#include <iostream>
#include <bitset>
#include <string>
#include <stdexcept>
#include <cstdint>

template <class T>
std::string getBitStr(const T& variable)
{
    std::string bitStr;
    const char* bits = reinterpret_cast<const char*>(&variable);
    for (size_t i = 0; i < sizeof(T); i ++)
    {
        // The bytes in bit string was stored in a reversed manner
        // Reverse back to make it human readable
        // Verify using https://www.binaryconvert.com/index.html
        bitStr = std::bitset<8>(bits[i]).to_string() + bitStr;
    }
    return bitStr;
}

void printBitStr(const std::string& bitStr)
{
    if (bitStr.length() % 8 != 0)
    {
        std::runtime_error{"Bit string length has to be multiples of 8!"};
    }
    for (size_t i = 0; i < bitStr.length(); i += 8)
    {
        std::cout << bitStr.substr(i,8) << " ";
    }
    std::cout << std::endl;
}

int main()
{
    uint8_t vUInt8 = 50;
    int vInt = 50;
    float vFloat = 50;
    double vDouble = 50;
    long vLong = 50;
    long long vLongLong = 50;

    printBitStr(getBitStr(vUInt8));
    printBitStr(getBitStr(vInt));
    printBitStr(getBitStr(vFloat));
    printBitStr(getBitStr(vDouble));
    printBitStr(getBitStr(vLong));
    printBitStr(getBitStr(vLongLong));
}

To compile the program, please run the following command in the terminal.

$ g++ reinterpret_cast.cpp -o reinterpret_cast --std=c++11

The expected output of the program would be as follows.

$ ./reinterpret_cast 
00110010 
00000000 00000000 00000000 00110010 
01000010 01001000 00000000 00000000 
01000000 01001001 00000000 00000000 00000000 00000000 00000000 00000000 
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00110010 
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00110010 

The long type and long long type are the exact same 64-bit integer type on my computer.

Conclusions

Understand exactly the cast you are trying to use before putting it into your code.