Type Casting in C++
Type casting is the process of converting one data type to another data type. C++ provides several ways to perform type casting, and each has different uses and levels of safety.
There are two main categories of type casting:
- Implicit conversion — A conversion performed automatically by the compiler. Example:
int x = 3.14;→xbecomes3(the fraction is lost). - Explicit conversion — A conversion performed manually by the programmer.
C++ provides four explicit casting operators:
static_cast— The most common compile-time cast. Safe for conversions that can be verified at compile time (int↔float, enum↔int, etc.).dynamic_cast— A runtime cast that requires RTTI (Run-Time Type Information). Only for pointers/references to polymorphic classes (having at least one virtual function).const_cast— Adds or removesconstfrom a variable. The only legal way to remove const-ness.reinterpret_cast— A low-level cast that reinterprets memory bits. Very dangerous and only used for very specific cases (pointer↔integer, bit manipulation).
The C-style cast (type)value is the old way that combines all kinds of casting. Avoid it because it is not safe and not clear.
Code Example
#include <iostream>
#include <string>
#include <typeinfo>
using namespace std;
// ==================== Base Class for Dynamic Cast ====================
class Shape {
public:
virtual void draw() const {
cout << "Drawing Shape" << endl;
}
virtual ~Shape() {}
};
class Circle : public Shape {
public:
void draw() const override {
cout << "Drawing Circle" << endl;
}
void drawCircle() const {
cout << "Circle: drawing a full circle" << endl;
}
};
class Rectangle : public Shape {
public:
void draw() const override {
cout << "Drawing Rectangle" << endl;
}
void drawRectangle() const {
cout << "Rectangle: drawing a rectangle" << endl;
}
};
int main() {
// ==================== Implicit Conversion ====================
cout << "=== Implicit Conversion ===" << endl;
int intVal = 42;
double doubleVal = intVal; // int → double (safe, no loss)
cout << "int → double: " << doubleVal << endl;
double pi = 3.14159;
int truncated = pi; // double → int (fraction lost)
cout << "double → int: " << truncated << endl;
bool flag = 100; // int → bool
cout << "int → bool: " << flag << endl;
char ch = 65; // int → char
cout << "int → char: " << ch << endl;
// ==================== static_cast ====================
cout << "\n=== static_cast ===" << endl;
// Numeric conversions
double d = 3.14159;
int n = static_cast<int>(d);
cout << "static_cast<int>(3.14159) = " << n << endl;
int val = 65;
char c = static_cast<char>(val);
cout << "static_cast<char>(65) = " << c << endl;
// Enum conversions
enum Color { RED, GREEN, BLUE };
int colorInt = static_cast<int>(RED);
cout << "static_cast<int>(RED) = " << colorInt << endl;
// Pointer conversions (inheritance)
Circle circle;
Shape* shapePtr = &circle; // Implicit upcast (safe)
Circle* circlePtr = static_cast<Circle*>(shapePtr); // Explicit downcast
circlePtr->drawCircle();
circlePtr->draw();
// Safe downcast (only if the type is already known)
Shape* shape = new Circle();
Circle* c1 = static_cast<Circle*>(shape);
c1->drawCircle();
delete shape;
// ==================== dynamic_cast ====================
cout << "\n=== dynamic_cast ===" << endl;
// Dynamic cast requires a polymorphic class (has virtual functions)
Shape* shapes[3];
shapes[0] = new Circle();
shapes[1] = new Rectangle();
shapes[2] = new Circle();
for (int i = 0; i < 3; i++) {
shapes[i]->draw(); // Virtual function call
// Try dynamic_cast to Circle
if (Circle* c = dynamic_cast<Circle*>(shapes[i])) {
c->drawCircle();
}
// Try dynamic_cast to Rectangle
else if (Rectangle* r = dynamic_cast<Rectangle*>(shapes[i])) {
r->drawRectangle();
}
delete shapes[i];
}
// Dynamic cast with a reference
try {
Circle realCircle;
Shape& shapeRef = realCircle;
// A failed reference dynamic_cast throws bad_cast
Rectangle& rectRef = dynamic_cast<Rectangle&>(shapeRef);
cout << "This will not be printed" << endl;
} catch (const bad_cast& e) {
cout << "bad_cast: " << e.what() << endl;
}
// Dynamic cast with nullptr
Shape* unknownShape = new Shape();
Circle* c2 = dynamic_cast<Circle*>(unknownShape);
if (c2 == nullptr) {
cout << "dynamic_cast to Circle failed: shape is not a Circle" << endl;
}
delete unknownShape;
// ==================== const_cast ====================
cout << "\n=== const_cast ===" << endl;
// Removing const
const int constVal = 42;
int* nonConstPtr = const_cast<int*>(&constVal);
// *nonConstPtr = 100; // UNDEFINED BEHAVIOR! Don't do this
cout << "const int: " << constVal << endl;
cout << "const_cast<int*>" << endl;
// Safe case: removing const from a function parameter
auto printString = [](const string& str) {
// const_cast for debugging (illustrative example)
string& mutableStr = const_cast<string&>(str);
cout << "String length: " << mutableStr.length() << endl;
};
string hello = "Hello";
printString(hello);
// const_cast for a legacy API that mislabels its parameters
void legacyFunction(char* str); // An old, wrongly-typed function
void safeCall(const char* input) {
// Only if you are sure the function does not modify the string
legacyFunction(const_cast<char*>(input));
}
// ==================== reinterpret_cast ====================
cout << "\n=== reinterpret_cast ===" << endl;
// Pointer to integer
int x = 42;
int* ptr = &x;
uintptr_t addr = reinterpret_cast<uintptr_t>(ptr);
cout << "Pointer address: " << addr << endl;
// Integer to pointer (very dangerous!)
// int* badPtr = reinterpret_cast<int*>(addr); // Dangerous
// Bit reinterpretation
float f = 3.14f;
int bits = reinterpret_cast<int&>(f);
cout << "Float " << f << " as int: " << bits << endl;
// char* to unsigned char*
const char* str = "Hello";
const unsigned char* ustr = reinterpret_cast<const unsigned char*>(str);
cout << "reinterpret_cast: " << ustr[0] << endl;
// ==================== C-style Cast (AVOID!) ====================
cout << "\n=== C-style Cast (Avoid!) ===" << endl;
int val2 = 42;
// C-style cast - combines all kinds of casting
double d2 = (double)val2; // Could be a static_cast
int* bad = (int*)0x12345678; // Could be a reinterpret_cast
cout << "(double)val2 = " << d2 << endl;
cout << "C-style casts are not safe and not clear" << endl;
// ==================== When to Use Each ====================
cout << "\n=== When to Use Each Cast ===" << endl;
cout << "1. static_cast: Numeric conversions, upcast/downcast inheritance that is already known." << endl;
cout << "2. dynamic_cast: Polymorphic downcast at runtime (requires RTTI)." << endl;
cout << "3. const_cast: Remove/add const (rarely needed, usually for legacy APIs)." << endl;
cout << "4. reinterpret_cast: Bit-level casting, pointer↔integer (very dangerous)." << endl;
cout << "5. Avoid C-style casts: (type)value is not safe and not clear." << endl;
// ==================== Safe Casting Rules ====================
cout << "\n=== Safe Casting Rules ===" << endl;
cout << "1. Use static_cast for basic numeric conversions." << endl;
cout << "2. Use dynamic_cast for polymorphic downcasts with checks." << endl;
cout << "3. Avoid reinterpret_cast except for low-level programming." << endl;
cout << "4. Use const_cast only to remove const from API errors." << endl;
cout << "5. Use enum class instead of plain enum to avoid enum casting." << endl;
cout << "6. Consider static_cast<void*> for generic pointers." << endl;
return 0;
}
Code Explanation
-
static_cast<int>(d)— A compile-time conversion fromdoubletoint. Safe because the compiler can verify the target type. It performs truncation (cutting off the decimal part). -
static_cast<Circle*>(shapePtr)— A downcast from a base class to a derived class. Only safe if the pointed-to object really is aCircle. There is no runtime check. -
dynamic_cast<Circle*>(shape)— A downcast with a runtime check. Returnsnullptrif the object is not aCircle. Requires RTTI and a polymorphic class (at least one virtual function). -
dynamic_cast<Circle&>(shapeRef)— A dynamic cast with a reference. If it fails, it throws abad_castexception (not nullptr, because a reference cannot be null). -
const_cast<int*>(&constVal)— Removes const from a pointer. Using the result to modify a const object is undefined behavior, unless the original object was not const. -
reinterpret_cast<uintptr_t>(ptr)— Converts a pointer to an integer. Useful for debugging or low-level memory address manipulation. -
reinterpret_cast<int&>(f)— Reinterprets the bits of a float as an integer. Useful for seeing the bit representation of a floating point number. -
(double)val— A C-style cast. Combines all kinds of casting into a single syntax. Not safe because it can perform unwanted conversions. -
bad_cast— An exception thrown when a reference dynamic cast fails. It must be caught with try-catch.
Everyday Use Cases
Imagine you are building a game system with many kinds of entities (monsters, NPCs, players). Each entity is a derivative of the base class Entity. You need to handle each entity type specifically:
class Entity {
public:
virtual void update() = 0;
virtual ~Entity() {}
};
class Monster : public Entity {
public:
void update() override { /* monster AI */ }
void attack() { cout << "Monster menyerang!" << endl; }
};
class NPC : public Entity {
public:
void update() override { /* NPC dialog */ }
void talk() { cout << "NPC: Welcome!" << endl; }
};
// In the game loop
for (Entity* entity : entities) {
entity->update(); // Virtual call, safe
// dynamic_cast to handle specific types
if (Monster* m = dynamic_cast<Monster*>(entity)) {
m->attack();
} else if (NPC* n = dynamic_cast<NPC*>(entity)) {
n->talk();
}
}
Conclusion
C++ provides four explicit casting operators: static_cast for safe compile-time conversions, dynamic_cast for polymorphic downcasts with runtime checks, const_cast for removing/adding const, and reinterpret_cast for dangerous bit-level casts. Avoid C-style casts because they are not safe and not clear. Use the most specific and safe cast for each situation.