C++ / INHERITANCE AND POLYMORPHISM
dynamic_cast and safe downcasting
Use dynamic_cast to convert a base pointer or reference down to a derived type only when the object really is that type, and handle the failure case.
What you will learn
- Downcast with dynamic_cast<D*> and test for nullptr before touching D's members
- Pick the pointer form for expected failure, the reference form to throw std::bad_cast
- Recognise that dynamic_cast needs a polymorphic source type with a virtual function
- Cross-cast sideways between sibling bases, which static_cast cannot express at all
Understanding dynamic_cast and safe downcasting
A pointer's static type is what you promised the compiler; the object's dynamic type is what actually got constructed. static_cast<Derived*>(base) adjusts the address using only the declared types and asks no questions, so if the object is not really a Derived you get a pointer into memory that never held one, and every use of it is undefined behaviour. dynamic_cast instead follows the object's vptr to the runtime type information the compiler emitted for the most-derived class, checks whether that object truly contains a Derived subobject, and only then hands back the adjusted address. That check is exactly why dynamic_cast requires a polymorphic source type: with no virtual function there is no vptr, nothing connects those bytes to a type, and the cast is rejected at compile time.
There are two forms because there are two sensible reactions to failure. The pointer form returns nullptr, which is why the idiomatic shape is a cast written inside an if condition: the conversion and the test happen once, and the derived pointer is only in scope in the branch where it is valid. The reference form has no null reference to return, so it throws std::bad_cast; reach for it when a mismatch means your assumptions are already broken and you want a loud, catchable failure instead of a silently skipped branch.
The useful mental model is that dynamic_cast interrogates the object, not the pointer: the answer depends only on which constructor ran, never on the type of the handle you happen to hold. Because the runtime knows the layout of the whole most-derived object, it can also cross-cast sideways from one base subobject to an unrelated sibling base of the same object, and it corrects the address when those subobjects live at different offsets, so the numeric pointer value can change. The flip side is cost and design: a failed cast may have to walk the inheritance graph, and a long if-else chain of dynamic_casts over a fixed set of derived classes is usually a virtual function nobody wrote.
<iostream>
<typeinfo>
struct Shape {
virtual ~Shape() = default;
virtual double area() const = 0;
};
struct Circle : Shape {
double radius;
explicit Circle(double r) : radius(r) {}
double area() const override { return 3.14159 * radius * radius; }
void printRadius() const { std::cout << "radius " << radius << '\n'; }
};
struct Square : Shape {
double side;
explicit Square(double s) : side(s) {}
double area() const override { return side * side; }
};
void inspect(const Shape& shape) {
if (const Circle* c = dynamic_cast<const Circle*>(&shape)) {
std::cout << "circle: ";
c->printRadius();
} else {
std::cout << "some other shape, area " << shape.area() << '\n';
}
}
int main() {
Circle circle(2.0);
Square square(3.0);
inspect(circle);
inspect(square);
const Shape& asShape = square;
try {
const Circle& c = dynamic_cast<const Circle&>(asShape);
std::cout << "unreachable " << c.radius << '\n';
} catch (const std::bad_cast&) {
std::cout << "reference form threw std::bad_cast\n";
}
}
dynamic_cast consults the object's real runtime type before converting, so a wrong downcast fails visibly instead of silently producing a pointer to something that is not there.
Worked examples
Cross-casting to an optional capability
Asks a Drawable whether the same object also happens to be Serializable, a sideways move static_cast cannot make.
<iostream>
struct Drawable {
virtual ~Drawable() = default;
virtual void draw() const = 0;
};
struct Serializable {
virtual ~Serializable() = default;
virtual void save() const = 0;
};
struct Sprite : Drawable, Serializable {
void draw() const override { std::cout << "draw sprite\n"; }
void save() const override { std::cout << "save sprite\n"; }
};
struct Cursor : Drawable {
void draw() const override { std::cout << "draw cursor\n"; }
};
void render(const Drawable& d) {
d.draw();
if (const Serializable* s = dynamic_cast<const Serializable*>(&d)) {
s->save();
} else {
std::cout << "nothing to save\n";
}
}
int main() {
Sprite sprite;
Cursor cursor;
render(sprite);
render(cursor);
}
Example explained
Line 1render sees only a Drawable&, so its static type carries no hint about saving.
Line 2dynamic_cast<const Serializable*>(&d) succeeds for Sprite because Sprite's runtime type information lists both bases; the returned address is normally not equal to &d, since the two base subobjects sit at different offsets.
Line 3For Cursor the identical expression yields nullptr: that object has no Serializable subobject at all, so the else branch runs.
Line 4static_cast here would not compile, because Drawable and Serializable are unrelated types; only the runtime check can link them through the shared most-derived object.
The derived part does not exist yet in a base constructor
Shows that during Base's constructor the object counts as a Base, so a downcast to Derived legitimately fails.
<iostream>
struct Base {
Base();
virtual ~Base() = default;
void report(const char* when) const;
};
struct Derived : Base {
Derived() { report("in Derived ctor body"); }
};
Base::Base() { report("in Base ctor body"); }
void Base::report(const char* when) const {
std::cout << when << ": "
<< (dynamic_cast<const Derived*>(this) ? "is Derived" : "not Derived")
<< '\n';
}
int main() {
Derived d;
d.report("after construction");
}
Example explained
Line 1Base::Base runs before the Derived part is initialised, and the standard says the object is then treated as a most-derived Base, so the cast is a defined nullptr rather than undefined behaviour.
Line 2The same report call from Derived's constructor body reports is Derived, because by then the vptr has been switched over to Derived's.
Line 3report has to be defined out of line: dynamic_cast<const Derived*> needs Derived to be a complete type, and a forward declaration would not compile.
Line 4This is the same reason base constructors cannot see derived behaviour through virtual calls; RTTI and virtual dispatch agree about how finished the object is.
Important notes
dynamic_cast can add const but never strip it, and the target class must be complete at the point of the cast, so a forward-declared Derived will not compile.
Builds compiled with RTTI disabled, such as -fno-rtti, reject dynamic_cast entirely; a failed cast can also walk the inheritance graph, so keep it out of tight loops.
Common mistakes
Reaching for static_cast<Derived*> on a base pointer that might not point to a Derived: it compiles without a warning, and reading a Derived member then interprets whatever bytes follow the base subobject, giving garbage values or a corrupted write instead of a detectable failure.
Chaining straight through the cast, as in dynamic_cast<Derived*>(p)->doThing(): when the type does not match you dereference nullptr, which usually crashes, and can appear to work in debug builds when doThing never touches a member.
Expecting the reference form to signal failure with a value: dynamic_cast<Derived&>(*p) throws std::bad_cast before you can test anything, and an uncaught throw calls std::terminate, so the reference form always needs a try block or a genuine guarantee.
Try it yourself
Change, predict, then run
Build a std::vector<std::unique_ptr<Shape>> holding a Circle(1.5), a Square(4.0) and a Circle(2.5), then write double totalCircleRadius(const std::vector<std::unique_ptr<Shape>>&) that dynamic_casts each element and adds radius only for circles. Print the total and confirm the square contributes nothing.
Open the C++ workspaceCheck your understanding
Why can dynamic_cast detect a bad downcast when static_cast cannot?
- dynamic_cast reads type information stored with the object at runtime, while static_cast decides everything at compile time from the declared pointer type and trusts the programmer
- dynamic_cast compares sizeof for the two classes and refuses the cast when they differ
- static_cast refuses to compile any downcast, so the mistake would have been caught earlier anyway
- dynamic_cast checks the pointer against a runtime registry of every live object in the program
Show answer
A polymorphic object carries a vptr that leads to type information describing its most-derived class, so dynamic_cast can compare that against the requested target and report failure with nullptr or std::bad_cast. Option 3 is tempting because static_cast is often described as the safe cast, but it happily compiles a downcast inside a known hierarchy and emits an unchecked offset adjustment, which is precisely why a wrong static_cast downcast is undefined behaviour rather than a detectable error.