C++ Casts

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
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
#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.

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.

const_cast.cpp
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 <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.

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.

dynamic_cast.cpp
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
#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.

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.

reinterpret_cast.cpp
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
39
40
41
42
43
44
45
46
47
48
49
50
#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.

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

The expected output of the program would be as follows.

1
2
3
4
5
6
7
$ ./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.

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.

reinterpret_cast.cpp
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 <iostream>

int main()
{
// 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<unsigned int&>(i) = 42;
std::cout << i << std::endl;
// Type aliasing through pointer.
// *reinterpret_cast<unsigned int*>(&i) is a lvalue.
*reinterpret_cast<unsigned int*>(&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;
}
1
2
3
4
5
$ g++ reinterpret_cast.cpp -o reinterpret_cast -std=c++11
$ ./reinterpret_cast
42
43
43

Conclusions

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

Author

Lei Mao

Posted on

03-04-2020

Updated on

07-20-2022

Licensed under


Comments