Question

C++ Casts Explained: C-Style Cast vs static_cast vs dynamic_cast

cpppointerscasting

Question

I have used C-style casts in C and C++ for many years, such as:

MyClass* m = (MyClass*)ptr;

In C++, I also see these alternatives:

MyClass* m = (MyClass*)ptr;
MyClass* m = static_cast<MyClass*>(ptr);
MyClass* m = dynamic_cast<MyClass*>(ptr);

What is the difference between these forms of casting in C++?

When should static_cast be used instead of a C-style cast, and when is dynamic_cast necessary?

What are the safety and runtime behavior differences between them?

Short Answer

By the end of this page, you will understand what type casting means in C++, why C-style casts are usually discouraged, how static_cast performs compile-time checked conversions, and how dynamic_cast safely checks polymorphic types at runtime. You will also learn when each cast is appropriate and how to avoid common casting mistakes.

Concept

In C++, a cast converts a value or pointer from one type to another. This is sometimes necessary, but it can also be dangerous if the conversion is not valid.

The three forms in this question are not equivalent:

  • C-style cast: (MyClass*)ptr
  • static_cast: static_cast<MyClass*>(ptr)
  • dynamic_cast: dynamic_cast<MyClass*>(ptr)

Why this matters

Types are one of the main ways C++ prevents bugs. A cast tells the compiler: "treat this value as another type." If that statement is wrong, your program may behave incorrectly or even crash.

C++ provides named casts because they make your intent clearer and separate safe conversions from dangerous conversions.

1. C-style cast

MyClass* m = (MyClass*)ptr;

This is the old C syntax. In C++, it is generally discouraged because it is too permissive.

A C-style cast may perform several different kinds of conversions behind the scenes, including ones similar to:

  • static_cast
  • const_cast
  • reinterpret_cast

That means it can hide unsafe operations in a short, unclear syntax.

2. static_cast

MyClass* m = static_cast<MyClass*>(ptr);

static_cast is used for conversions that are known and checked at compile time.

Typical uses:

  • numeric conversions such as double to int
  • converting void* to a specific pointer type
  • upcasting in class hierarchies, such as Derived* to Base*
  • downcasting only when you are certain the object is really of the target type

Important: static_cast does not check object type at runtime when downcasting through inheritance.

If you cast a Base* to Derived* using static_cast, and the object is not actually a Derived, the behavior is invalid.

3. dynamic_cast

auto m = dynamic_cast<MyClass*>(ptr);

dynamic_cast is for safe runtime checking in inheritance hierarchies.

It is mainly used when:

  • you have a pointer or reference to a base class
  • the base class is polymorphic (usually meaning it has at least one virtual function)
  • you want to know whether the object is really a certain derived type

For pointer casts:

  • success: returns a valid pointer
  • failure: returns nullptr

For reference casts:

  • success: returns the reference
  • failure: throws std::bad_cast

Key idea

  • Use static_cast when the conversion is logically valid and does not require runtime type checking.
  • Use dynamic_cast when converting within a polymorphic class hierarchy and you need runtime safety.
  • Avoid C-style casts in modern C++ because they are less explicit and easier to misuse.

Mental Model

Think of casting like entering a building with different access methods.

  • A C-style cast is like using a master key that opens many doors, even doors you should not open. It is powerful, but risky.
  • A static_cast is like using the correct badge for a door that the building plan already says you may access. The check happens from the plan, not in real time.
  • A dynamic_cast is like a security guard checking your identity at the door right now. If you are not the right person, access is denied.

Another way to think about it:

  • static_cast says: "I know what this is."
  • dynamic_cast says: "Please verify what this really is."
  • C-style cast says: "Just do something that makes this compile."
Take Quiz

Syntax and Examples

Core syntax

// C-style cast
TargetType value = (TargetType)expression;

// static_cast
TargetType value = static_cast<TargetType>(expression);

// dynamic_cast
TargetType value = dynamic_cast<TargetType>(expression);

Example: static_cast with numbers

double price = 19.99;
int whole = static_cast<int>(price);

whole becomes 19. This is a normal compile-time conversion.

Example: upcasting with inheritance

class Animal {
public:
    virtual ~Animal() = default;
};

class Dog : public Animal {
};

Dog dog;
Animal* animalPtr = static_cast<Animal*>(&dog);

This is safe because every Dog is an Animal.

Example: unsafe downcast with static_cast

class Animal {
public:
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void bark() {}
};

class Cat : public Animal {
};

Animal* animal = new Cat();
Dog* dog = static_cast<Dog*>(animal); // Compiles, but unsafe
// dog->bark(); // Undefined behavior if used

This compiles, but the object is actually a Cat, not a Dog.

Example: safe downcast with dynamic_cast

#include <iostream>

class Animal {
public:
    virtual ~Animal() = default;
};

class Dog : public Animal {
public:
    void bark() {
        std::cout << "Woof\n";
    }
};

class Cat : public Animal {
};

int main() {
    Animal* animal = new Cat();

    Dog* dog = dynamic_cast<Dog*>(animal);

    if (dog) {
        dog->bark();
    } else {
        std::cout << "animal is not a Dog\n";
    }

    delete animal;
}

Here, dynamic_cast checks the real runtime type. Since animal points to a Cat, the cast fails and returns nullptr.

Example: C-style cast hiding danger

Animal* animal = new Cat();
Dog* dog = (Dog*)animal; // Looks simple, but dangerous

This hides the same risk as the unsafe static_cast, and possibly more.

Step by Step Execution

Consider this example:

#include <iostream>

class Base {
public:
    virtual ~Base() = default;
};

class Derived : public Base {
public:
    void hello() {
        std::cout << "Hello from Derived\n";
    }
};

int main() {
    Base* ptr = new Derived();

    Derived* a = static_cast<Derived*>(ptr);
    if (a) {
        a->hello();
    }

    Derived* b = dynamic_cast<Derived*>(ptr);
    if (b) {
        b->hello();
    }

    delete ptr;
}

What happens step by step

  1. Base* ptr = new Derived();

    • A Derived object is created.
    • It is stored in a Base* pointer.
    • This is valid because Derived inherits from Base.
  2. Derived* a = static_cast<Derived*>(ptr);

    • The compiler allows the conversion.
    • No runtime check is performed here.
    • In this case, it happens to be correct because ptr really points to a Derived.
  3. if (a)

    • a is not null, so the condition is true.
    • Note: this does not prove the cast was safe. It only means the pointer itself is non-null.
  4. a->hello();

    • This works because the object really is a Derived.
  5. Derived* b = dynamic_cast<Derived*>(ptr);

    • Runtime type information is used to check whether ptr really refers to a Derived.
    • It does, so the cast succeeds.
  6. if (b)

    • b is valid, so the condition is true.
  7. b->hello();

    • This also works.

If the object were not Derived

If ptr pointed to a plain Base object instead:

  • static_cast<Derived*>(ptr) would still compile, but using the result would be unsafe.
  • dynamic_cast<Derived*>(ptr) would return nullptr.

That is the main difference: dynamic_cast verifies at runtime, while static_cast does not.

Real World Use Cases

Where static_cast is commonly used

Numeric conversions

double ratio = 3.8;
int count = static_cast<int>(ratio);

Useful when parsing values, formatting output, or converting between numeric types.

Converting void* from low-level APIs

void* raw = get_buffer();
char* bytes = static_cast<char*>(raw);

This appears in older APIs, callback systems, and C libraries.

Upcasting in class hierarchies

Derived d;
Base* base = static_cast<Base*>(&d);

This happens frequently when storing derived objects behind base-class interfaces.

Where dynamic_cast is commonly used

Working with plugin or framework object hierarchies

A framework may give you a Base*, and you may need to check whether it is actually a more specific subtype.

Processing mixed collections of polymorphic objects

std::vector<Animal*> animals;

If you need dog-specific behavior only for Dog objects, dynamic_cast can safely test each item.

Optional subtype behavior

When some objects support extra behavior and others do not, dynamic_cast can detect that at runtime.

Where C-style casts still appear

  • legacy C++ codebases
  • code originally written in C
  • quick examples or old tutorials

In modern C++, named casts are preferred because they are easier to review and safer to maintain.

Take Quiz

Real Codebase Usage

In real C++ projects, developers usually prefer clear and narrow casts.

Common patterns

1. Prefer no cast if design can avoid it

Good class design often removes the need for downcasting entirely.

For example, virtual functions are often better than checking types manually:

class Animal {
public:
    virtual ~Animal() = default;
    virtual void speak() = 0;
};

class Dog : public Animal {
public:
    void speak() override {
        std::cout << "Woof\n";
    }
};

Instead of casting to Dog, call speak() through the base interface.

2. Use static_cast for explicit, expected conversions

Developers often use static_cast when they want a visible conversion in code review:

int index = static_cast<int>(items.size());

This makes narrowing conversions deliberate rather than accidental.

3. Use dynamic_cast when runtime type checks are part of the design

Base* obj = createObject();
if (auto* special = dynamic_cast<SpecialType*>(obj)) {
    special->runSpecialLogic();
}

This pattern is common in editor tools, GUI frameworks, compilers, and game engines.

4. Guard clauses after dynamic_cast

auto* dog = dynamic_cast<Dog*>(animal);
if (!dog) {
    return;
}

dog->bark();

This keeps the safe path clear and avoids invalid use.

5. Avoid C-style casts in modern code reviews

Many teams treat C-style casts as a code smell because they can hide const removal, reinterpretation, or unsafe hierarchy conversions.

Common Mistakes

1. Using static_cast for unsafe downcasting

Broken example:

Animal* animal = new Cat();
Dog* dog = static_cast<Dog*>(animal);
dog->bark(); // Undefined behavior

Why it is wrong:

  • static_cast does not verify that animal really points to a Dog.

How to avoid it:

Dog* dog = dynamic_cast<Dog*>(animal);
if (dog) {
    dog->bark();
}

2. Forgetting that dynamic_cast needs a polymorphic base class

Broken example:

class Base {};
class Derived : public Base {};

Base* ptr = new Derived();
Derived* d = dynamic_cast<Derived*>(ptr); // Error

Why it is wrong:

  • dynamic_cast for downcasting requires runtime type information.
  • The base class must be polymorphic, usually by having a virtual function.

Fix:

class Base {
public:
    virtual ~Base() = default;
};

3. Assuming static_cast returning a non-null pointer means success

Derived* d = static_cast<Derived*>(basePtr);
if (d) {
    // This does not prove the object is actually Derived
}

A non-null pointer only means the original pointer was non-null.

4. Using C-style casts because they are shorter

Broken example:

MyClass* m = (MyClass*)ptr;

Why it is a problem:

  • It does not communicate what kind of conversion is happening.
  • It can silently perform dangerous conversions.

Better:

MyClass* m = static_cast<MyClass*>(ptr);

or

MyClass* m = dynamic_cast<MyClass*>(ptr);

Choose based on whether runtime checking is needed.

5. Overusing dynamic_cast

If you constantly cast between types, your class design may need improvement.

Often a virtual function, interface, or better abstraction is cleaner than repeated runtime type checks.

Comparisons

Cast typeChecked at compile timeChecked at runtimeTypical useRisk level
C-style castSometimesNo clear runtime safetyLegacy code, old C syntaxHigh
static_castYesNoNumeric conversion, upcasting, known safe conversionMedium
dynamic_castYesYesSafe downcasting in polymorphic hierarchiesLower

static_cast vs dynamic_cast

Featurestatic_castdynamic_cast
Runtime type checkNoYes
Requires polymorphic base class for downcastNoYes
Performance costUsually lowerUsually higher
Safe for uncertain downcastNoYes
Returns nullptr on failed pointer castNoYes

C-style cast vs C++ named casts

StyleBenefitDrawback
C-style castShortToo ambiguous and can hide unsafe conversions
C++ named castsExplicit intentSlightly longer syntax

Practical rule of thumb

  • Prefer static_cast for ordinary explicit conversions.
  • Use dynamic_cast when type safety depends on the object's real runtime type.
  • Avoid C-style casts in modern C++ code.
Take Quiz

Cheat Sheet

Quick rules

  • Prefer named C++ casts over C-style casts.
  • Use static_cast for explicit, expected conversions.
  • Use dynamic_cast for safe runtime-checked downcasting.
  • dynamic_cast works with polymorphic base classes.
  • Failed dynamic_cast on pointers returns nullptr.
  • Failed dynamic_cast on references throws std::bad_cast.

Syntax

T value = static_cast<T>(expr);
T value = dynamic_cast<T>(expr);
T value = (T)expr; // C-style cast, usually avoid in C++

Safe examples

int x = static_cast<int>(3.14);
Base* b = static_cast<Base*>(&derivedObj);
if (Derived* d = dynamic_cast<Derived*>(basePtr)) {
    d->run();
}

Warning signs

  • Downcasting with static_cast when the real type is uncertain
  • Frequent C-style casts in C++ code
  • Using dynamic_cast on a non-polymorphic base class
  • Repeated casts that suggest missing virtual functions

Memory aid

  • static_cast = trust the type relationship
  • dynamic_cast = verify the type at runtime
  • C-style cast = unclear shortcut, avoid when possible

FAQ

What is the difference between static_cast and dynamic_cast in C++?

static_cast performs compile-time conversions without checking the real runtime object type. dynamic_cast checks the actual runtime type and is safer for downcasting in inheritance hierarchies.

Is a C-style cast the same as static_cast?

No. A C-style cast can behave like static_cast, but it can also perform more dangerous kinds of conversions. That is why it is less safe and less explicit.

When should I use dynamic_cast?

Use it when you have a base-class pointer or reference and need to safely determine whether the object is really a specific derived type at runtime.

Why does dynamic_cast require a virtual function?

It needs runtime type information, which is available for polymorphic classes. A class becomes polymorphic when it has at least one virtual function.

Does dynamic_cast have a performance cost?

Yes, usually more than static_cast, because it performs a runtime check. In most code, correctness matters more than this small cost unless profiling shows otherwise.

Should I avoid all casts in C++?

Not all casts, but unnecessary casts should be avoided. Many conversions are fine, especially with static_cast. Unsafe or frequent downcasts may indicate a design issue.

What happens if dynamic_cast fails?

For pointers, it returns nullptr. For references, it throws std::bad_cast.

Which cast should I prefer in modern C++?

Usually static_cast for normal explicit conversions, dynamic_cast for safe runtime-checked hierarchy conversions, and avoid C-style casts unless there is a very specific reason.

Take Quiz

Mini Project

Description

Build a small C++ program that stores different animal types through base-class pointers, then demonstrates the difference between safe runtime checking with dynamic_cast and unchecked downcasting with static_cast. This helps you see exactly why dynamic_cast is useful in real inheritance code.

Goal

Create a polymorphic class hierarchy, store mixed objects in a collection, and safely run subtype-specific behavior only when the object really matches the target type.

Requirements

  1. Create a base class with at least one virtual function.
  2. Create at least two derived classes, such as Dog and Cat.
  3. Store derived objects in a container of base-class pointers.
  4. Use dynamic_cast to detect one specific derived type safely.
  5. Print different output depending on whether the cast succeeds or fails.
Take Quiz

Keep learning

Related questions

View more