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.
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.
1
$ g++ static_cast.cpp -o static_cast -std=c++11
The expected output of the program would be as follows.
1 2 3 4 5 6 7
$ ./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.
int vInt = 1; constint* 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 constint vIntConst = 1; constint* 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.
1
$ g++ const_cast.cpp -o const_cast -std=c++11
The expected output of the program would be as follows.
1 2 3 4 5 6
$ ./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.
classA { public: virtualvoidfunc_virtual() { std::cout << "Virtual method from class A" << std::endl; } voidfunc_non_virtual() { std::cout << "Non-virtual method from class A" << std::endl; }
};
classB: public A { public: virtualvoidfunc_virtual() { std::cout << "Virtual method from class B" << std::endl; } voidfunc_non_virtual() { std::cout << "Non-virtual method from class B" << std::endl; }
};
classC: public A { public: virtualvoidfunc_virtual() { std::cout << "Virtual method from class C" << std::endl; } voidfunc_non_virtual() { std::cout << "Non-virtual method from class C" << std::endl; }
};
classD: public B, public C { public: virtualvoidfunc_virtual() { std::cout << "Virtual method from class D" << std::endl; } voidfunc_non_virtual() { std::cout << "Non-virtual method from class D" << std::endl; } };
intmain() { 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.
1
$ g++ dynamic_cast.cpp -o dynamic_cast -std=c++11
The expected output of the program would be as follows.
1 2 3 4 5 6 7 8 9 10 11 12
$ ./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.
template <classT> std::string getBitStr(const T& variable) { std::string bitStr; constchar* bits = reinterpret_cast<constchar*>(&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; }
voidprintBitStr(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; }
The long type and long long type are the exact same 64-bit integer type on my computer.
reinterpret_cast can also be casted to reference and it is equivalent as casting to a pointer followed by dereference. Other than casting to pointer and reference, reinterpret_cast can only do a limited number of casts.
intmain() { // As with all cast expressions, the result is: // an lvalue if new-type is a reference type; // an rvalue otherwise. int i{7}; // Type aliasing through reference. // reinterpret_cast<unsigned int&>(i) is a lvalue. reinterpret_cast<unsignedint&>(i) = 42; std::cout << i << std::endl; // Type aliasing through pointer. // *reinterpret_cast<unsigned int*>(&i) is a lvalue. *reinterpret_cast<unsignedint*>(&i) = 43; std::cout << i << std::endl; // This will not compile. // reinterpret_cast<unsigned int>(i) = 42; // https://en.cppreference.com/w/cpp/language/reinterpret_cast // An expression of integral, enumeration, pointer, or pointer-to-member // type can be converted to its own type. Other conversions all involves // pointer or reference. So the only non-pointer or non-reference conversion // we could do is int j{0}; // reinterpret_cast<int>(i) is a rvalue and cannot be modified. j = reinterpret_cast<int>(i); std::cout << j << std::endl; }