Skillversitas

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; → x becomes 3 (the fraction is lost).
  • Explicit conversion — A conversion performed manually by the programmer.

C++ provides four explicit casting operators:

  1. static_cast — The most common compile-time cast. Safe for conversions that can be verified at compile time (int↔float, enum↔int, etc.).
  2. 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).
  3. const_cast — Adds or removes const from a variable. The only legal way to remove const-ness.
  4. 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

  1. static_cast<int>(d) — A compile-time conversion from double to int. Safe because the compiler can verify the target type. It performs truncation (cutting off the decimal part).

  2. static_cast<Circle*>(shapePtr) — A downcast from a base class to a derived class. Only safe if the pointed-to object really is a Circle. There is no runtime check.

  3. dynamic_cast<Circle*>(shape) — A downcast with a runtime check. Returns nullptr if the object is not a Circle. Requires RTTI and a polymorphic class (at least one virtual function).

  4. dynamic_cast<Circle&>(shapeRef) — A dynamic cast with a reference. If it fails, it throws a bad_cast exception (not nullptr, because a reference cannot be null).

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

  6. reinterpret_cast<uintptr_t>(ptr) — Converts a pointer to an integer. Useful for debugging or low-level memory address manipulation.

  7. reinterpret_cast<int&>(f) — Reinterprets the bits of a float as an integer. Useful for seeing the bit representation of a floating point number.

  8. (double)val — A C-style cast. Combines all kinds of casting into a single syntax. Not safe because it can perform unwanted conversions.

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