What Is a Dynamic Cast?

Every cast you have met so far is a promise you make to the compiler. static_cast converts a value because you said the conversion makes sense, and the generated code contains no evidence that anyone checked.

dynamic_cast is the exception. It converts a pointer or reference within a class hierarchy, and before handing back the converted result it asks the object at the other end what type it really is. If the answer does not match, the conversion does not happen and you are told so. That single property, a conversion that can report failure, is what the rest of this lesson is about.

The price is that dynamic_cast only works where an object can answer the question. That means a polymorphic hierarchy: the class you are casting from must have at least one virtual function.

Two directions through a hierarchy

A hierarchy gives every object more than one usable type. A SolventBundle in the laundry example below is also a Bundle, so a pointer or reference to it can be viewed either way.

Converting a derived class pointer or reference into a base class one is upcasting. It is always safe, because the derived object genuinely contains a base subobject, so C++ performs it implicitly with no cast written at all. Storing a SolventBundle* in a Bundle*, or passing a SolventBundle to a function taking const Bundle&, are both upcasts.

Converting a base class pointer or reference back into a derived class one is downcasting. This one is not automatic, because it may be a lie: the base pointer might be pointing at a plain base object, or at a sibling type entirely. C++ makes you ask for it explicitly.

Downcasting exists because a base pointer knows less than the object behind it. Here is a laundry intake that classifies incoming work. Solvent cleaning records which solvent the garments went into, and a plain water wash has no solvent at all, so the accessor lives only on the derived class.

The following program is broken. Do not use it as a model.

#include <iostream>
#include <memory>
#include <string>
#include <string_view>

class Bundle
{
public:
    explicit Bundle(int kilograms)
        : m_kilograms{ kilograms }
    {
    }

    virtual ~Bundle() = default;

    int getKilograms() const { return m_kilograms; }

protected:
    int m_kilograms{};
};

class SolventBundle : public Bundle
{
public:
    SolventBundle(int kilograms, std::string_view solvent)
        : Bundle{ kilograms }, m_solvent{ solvent }
    {
    }

    const std::string& getSolvent() const { return m_solvent; }

private:
    std::string m_solvent{};
};

std::unique_ptr<Bundle> intake(bool needsSolvent)
{
    if (needsSolvent)
        return std::make_unique<SolventBundle>(9, "hydrocarbon");

    return std::make_unique<Bundle>(4);
}

int main()
{
    std::unique_ptr<Bundle> item{ intake(true) };

    std::cout << item->getKilograms() << " kg bundle, solvent: " << item->getSolvent() << '\n';

    return 0;
}

intake() really did produce a SolventBundle, but that fact is gone by the time main() looks at the result. Name lookup happens on the static type, and the static type is Bundle:

s.cpp: In function 'int main()':
s.cpp:48:75: error: 'class Bundle' has no member named 'getSolvent'
   48 |     std::cout << item->getKilograms() << " kg bundle, solvent: " << item->getSolvent() << '\n';
      |                                                                           ^~~~~~~~~~

One escape is to push getSolvent() up into Bundle as a virtual function, but then every plain wash has to invent a return value for a question that does not apply to it, and the base class grows a member that describes only one branch of the hierarchy. The other escape is to recover the derived type, which is what dynamic_cast does.

What the runtime check actually reads

Run-time type information, almost always written RTTI, is the C++ feature that keeps information about an object's type available while the program runs. It is what makes the question "what are you, really?" answerable at all, and both dynamic_cast and the typeid operator are built on it.

You already know where it lives. A polymorphic class gives each object a hidden pointer to its class's virtual table, and the table is per class, not per object. Implementations park a type description next to that table describing the class name, its bases, and the offsets between subobjects. Following the object's virtual table pointer therefore leads to a full account of the object's real type, whatever the pointer you started from claimed.

A dynamic_cast follows that trail and walks the recorded base classes looking for the target type. That is real work at run time, proportional to the shape of the hierarchy, which is the cost you are paying for the check. A static_cast down the same hierarchy compiles to an address adjustment and sometimes to nothing at all.

Key Concept
The check needs a polymorphic operand, not a polymorphic target. What matters is that the class you cast from has a virtual function, because that is the class supplying the virtual table pointer the lookup starts at.
Warning
RTTI costs space in the binary, and most compilers offer a switch to remove it, such as -fno-rtti on GCC and Clang. With RTTI disabled there is nothing for the check to read, so dynamic_cast stops being usable. Embedded projects that build this way have to solve type recovery another way.

The four results of a dynamic_cast

One operator, two operand kinds, two outcomes each. Everything dynamic_cast does when a downcast is involved fits in this table.

Expression Object really is a SolventBundle Object is something else
dynamic_cast<SolventBundle*>(pointer) Pointer to the derived object nullptr
dynamic_cast<SolventBundle&>(reference) Reference to the derived object Throws std::bad_cast

The split exists because C++ has no null reference to hand back. A pointer has a spare value meaning "nothing", so failure is reported in the return value. A reference has no such value, so failure has to be reported by throwing.

Both failures are defined behavior. Neither one is a crash, and neither one leaves you with a half-converted pointer. What crashes is what you do afterwards if you ignore the result.

Casting a pointer

Give the operator the target pointer type and the base pointer, and test what comes back before using it. Casting away from a const Bundle& needs a const target type too, since a cast cannot quietly drop const.

#include <iostream>
#include <memory>
#include <string>
#include <string_view>

class Bundle
{
public:
    explicit Bundle(int kilograms)
        : m_kilograms{ kilograms }
    {
    }

    virtual ~Bundle() = default;

    int getKilograms() const { return m_kilograms; }

protected:
    int m_kilograms{};
};

class SolventBundle : public Bundle
{
public:
    SolventBundle(int kilograms, std::string_view solvent)
        : Bundle{ kilograms }, m_solvent{ solvent }
    {
    }

    const std::string& getSolvent() const { return m_solvent; }

private:
    std::string m_solvent{};
};

std::unique_ptr<Bundle> intake(bool needsSolvent)
{
    if (needsSolvent)
        return std::make_unique<SolventBundle>(9, "hydrocarbon");

    return std::make_unique<Bundle>(4);
}

void printTicket(const Bundle& item)
{
    const SolventBundle* solventItem{ dynamic_cast<const SolventBundle*>(&item) };

    std::cout << item.getKilograms() << " kg bundle, ";

    if (solventItem)
        std::cout << "solvent: " << solventItem->getSolvent() << '\n';
    else
        std::cout << "washed in water" << '\n';
}

int main()
{
    std::unique_ptr<Bundle> coats{ intake(true) };
    std::unique_ptr<Bundle> towels{ intake(false) };

    printTicket(*coats);
    printTicket(*towels);

    return 0;
}

Output:

9 kg bundle, solvent: hydrocarbon
4 kg bundle, washed in water

printTicket() never learns which factory call produced its argument, and it does not need to. The first call gets a usable SolventBundle* and prints the solvent. The second gets nullptr and takes the other branch. Both results are correct answers, and the if is the whole of the error handling.

Best Practice
Treat the result of a pointer dynamic_cast as a value that has to be tested, in the same way you would test the result of a lookup that can miss. Write the null branch at the moment you write the cast, not afterwards.

Skipping the null check

Nothing forces you to test the result, and the cast that returns nullptr is perfectly happy to do so silently. What follows is the failure that costs you.

The following program is broken. Do not use it as a model.

int main()
{
    std::unique_ptr<Bundle> towels{ intake(false) };

    const SolventBundle* solventItem{ dynamic_cast<const SolventBundle*>(towels.get()) };

    std::cout << "solvent: " << solventItem->getSolvent() << '\n';

    return 0;
}

The cast behaves exactly as specified: towels owns a plain Bundle, the conversion is impossible, and solventItem is set to nullptr. The bug is on the next line, where a null pointer is dereferenced.

Danger
Dereferencing the null result of a failed dynamic_cast is undefined behavior. It has no output to show, because the program has no defined meaning. It may crash immediately, or with optimizations enabled it may be compiled into something that appears to work until the surrounding code changes.

Casting a reference

References support the same conversion with the same runtime check. Only the failure report differs, and it differs because a reference always denotes an object.

#include <iostream>
#include <string>
#include <string_view>
#include <typeinfo>

class Bundle
{
public:
    explicit Bundle(int kilograms)
        : m_kilograms{ kilograms }
    {
    }

    virtual ~Bundle() = default;

    int getKilograms() const { return m_kilograms; }

protected:
    int m_kilograms{};
};

class SolventBundle : public Bundle
{
public:
    SolventBundle(int kilograms, std::string_view solvent)
        : Bundle{ kilograms }, m_solvent{ solvent }
    {
    }

    const std::string& getSolvent() const { return m_solvent; }

private:
    std::string m_solvent{};
};

int main()
{
    SolventBundle jacket{ 3, "perchloroethylene" };
    Bundle towels{ 4 };

    const Bundle& firstSlot{ jacket };
    const SolventBundle& cleared{ dynamic_cast<const SolventBundle&>(firstSlot) };
    std::cout << "reference cast succeeded, solvent: " << cleared.getSolvent() << '\n';

    const Bundle& secondSlot{ towels };
    try
    {
        const SolventBundle& refused{ dynamic_cast<const SolventBundle&>(secondSlot) };
        std::cout << "never printed: " << refused.getSolvent() << '\n';
    }
    catch (const std::bad_cast&)
    {
        std::cout << "reference cast threw std::bad_cast" << '\n';
    }

    return 0;
}

Output:

reference cast succeeded, solvent: perchloroethylene
reference cast threw std::bad_cast

std::bad_cast is declared in <typeinfo>, so that header has to be included to catch it by type.

Related Content
Exceptions get a chapter of their own later in the course. All you need here is the shape: the guarded work goes in the try block, and control jumps to the matching catch when a throw happens inside it. Without the try, an uncaught std::bad_cast ends the program through std::terminate.

The practical consequence is that the pointer form is the one to reach for when failure is an ordinary outcome you plan to handle, since a test is cheaper to write and read than a handler. The reference form suits the case where a mismatch means the program is already wrong and you want it to say so loudly.

Where dynamic_cast refuses to work

The check depends on the hierarchy, and three shapes of hierarchy cannot support it.

Shape What happens
The class you cast from has no virtual functions Rejected at compile time: there is no virtual table pointer to read
The inheritance path is private or protected The base is not a public base of the target, so the check finds no match
A virtual base leaves more than one candidate subobject The path is ambiguous, so the cast fails rather than guessing

The first one is a compile error, so you find out immediately. This hierarchy has ordinary inheritance and no virtual member anywhere, which makes it a non-polymorphic type.

The following program is broken. Do not use it as a model.

#include <iostream>

class Hamper
{
public:
    explicit Hamper(int slots)
        : m_slots{ slots }
    {
    }

    int getSlots() const { return m_slots; }

protected:
    int m_slots{};
};

class SortedHamper : public Hamper
{
public:
    SortedHamper(int slots, int lanes)
        : Hamper{ slots }, m_lanes{ lanes }
    {
    }

    int getLanes() const { return m_lanes; }

private:
    int m_lanes{};
};

int main()
{
    SortedHamper rack{ 12, 3 };
    Hamper* trolley{ &rack };

    SortedHamper* sorted{ dynamic_cast<SortedHamper*>(trolley) };
    std::cout << sorted->getLanes() << '\n';

    return 0;
}
s.cpp: In function 'int main()':
s.cpp:36:27: error: cannot 'dynamic_cast' 'trolley' (of type 'class Hamper*') to type 'class SortedHamper*' (source type is not polymorphic)
   36 |     SortedHamper* sorted{ dynamic_cast<SortedHamper*>(trolley) };
      |                           ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The object genuinely is a SortedHamper, and the cast is still rejected, because nothing in a Hamper records that. Adding a virtual destructor to Hamper fixes it, which is another reason base classes want one.

The other two rows fail at run time instead. Non-public inheritance is the surprising one: the object is the right type, but the check only accepts a public base subobject, so the cast reports failure exactly as if the types were unrelated.

Hand-rolled type tags and why they drift

Downcasting is also possible with static_cast, which skips the check entirely.

static_cast<SolventBundle*> dynamic_cast<SolventBundle*>
When the type is verified Never At run time, on every cast
Requires a polymorphic operand No Yes
Cost An address adjustment, often free Reads the type record and walks the bases
If the object is not a SolventBundle Compiles, "succeeds", undefined behavior on use Yields nullptr

static_cast is the right choice only when you have already established the type by other means. The usual homemade way to establish it is a tag: an enumerator per class, returned by a virtual function, compared before casting. It works, until the hierarchy grows a third level.

#include <iostream>
#include <string>
#include <string_view>

enum class CleanID
{
    plainWash,
    solventWash,
    suedeWash,
};

class Bundle
{
public:
    explicit Bundle(int kilograms)
        : m_kilograms{ kilograms }
    {
    }

    virtual ~Bundle() = default;

    virtual CleanID getCleanID() const { return CleanID::plainWash; }

    int getKilograms() const { return m_kilograms; }

protected:
    int m_kilograms{};
};

class SolventBundle : public Bundle
{
public:
    SolventBundle(int kilograms, std::string_view solvent)
        : Bundle{ kilograms }, m_solvent{ solvent }
    {
    }

    CleanID getCleanID() const override { return CleanID::solventWash; }

    const std::string& getSolvent() const { return m_solvent; }

private:
    std::string m_solvent{};
};

class SuedeBundle : public SolventBundle
{
public:
    explicit SuedeBundle(int kilograms)
        : SolventBundle{ kilograms, "hydrocarbon" }
    {
    }

    CleanID getCleanID() const override { return CleanID::suedeWash; }
};

void tagRoute(const Bundle& item)
{
    if (item.getCleanID() == CleanID::solventWash)
    {
        const SolventBundle* solventItem{ static_cast<const SolventBundle*>(&item) };
        std::cout << "tag route sends it to " << solventItem->getSolvent() << '\n';
    }
    else
        std::cout << "tag route sends it to the water line" << '\n';
}

void castRoute(const Bundle& item)
{
    const SolventBundle* solventItem{ dynamic_cast<const SolventBundle*>(&item) };

    if (solventItem)
        std::cout << "cast route sends it to " << solventItem->getSolvent() << '\n';
    else
        std::cout << "cast route sends it to the water line" << '\n';
}

int main()
{
    SuedeBundle jacket{ 2 };

    tagRoute(jacket);
    castRoute(jacket);

    return 0;
}

Output:

tag route sends it to the water line
cast route sends it to hydrocarbon

A SuedeBundle is a SolventBundle, and it is holding a solvent, yet the tag comparison sends it to the water line. The tag reports one exact class, so it answers "is it exactly this?", while the question the code meant to ask was "is it this or anything derived from it?". dynamic_cast asks the second question by construction, and it keeps answering correctly as classes are added, with no table of enumerators to maintain.

Warning
Every hand-rolled tag scheme has this failure mode, and it appears at the moment someone adds a subclass, usually far from the routing code. If you find yourself writing a virtual function whose only job is to report which class this is, you have written a slower, less reliable dynamic_cast.

Virtual function or downcast?

Both tools reach derived behavior from a base handle, and the choice between them is a design question rather than a technical one. A virtual function moves the decision into the classes, so the calling code stays free of type names. A downcast keeps the decision in the calling code, which is worth doing only when the classes cannot reasonably hold it.

Situation Reach for
Behavior every subtype can define in its own terms A virtual function
A base class you cannot edit, such as one from the standard library or a third-party header dynamic_cast
An accessor or data that exists on one branch only, with no meaning elsewhere dynamic_cast
Behavior no base class object could sensibly provide, where the base is never instantiated A pure virtual function

Casting sprinkled through the code, especially a chain of dynamic_cast tests that ends in an else for the base case, is usually a virtual function that was never written. One cast at a boundary, where a general handle meets code that genuinely needs one specific type, is not.

Best Practice
Try to express the operation as a virtual function first. Use dynamic_cast when the base class is out of your control, or when what you need belongs to one derived class and nowhere else, and use static_cast for downcasting only when the type is already proven and the check is measurably too expensive.

Summary

Two directions: converting a derived pointer or reference to a base one is upcasting, it is always valid, and C++ does it implicitly. Converting a base pointer or reference back to a derived one is downcasting, it may not be valid, and it has to be requested explicitly.

What the operator does: dynamic_cast performs a downcast with a runtime check. It reads the type recorded for the object, looks for the target type among the object's base classes, and reports failure instead of producing an unusable result.

Failure has two forms: a failed pointer cast evaluates to nullptr. A failed reference cast throws std::bad_cast, declared in <typeinfo>, because C++ has no null reference to return. Both outcomes are well defined.

The result is not usable until it is checked: test a pointer result for null before dereferencing it, or catch std::bad_cast around a reference cast. Dereferencing the null result of a failed cast is undefined behavior.

RTTI is the underlying feature: run-time type information is what exposes an object's real type while the program runs, stored alongside the virtual table and used by dynamic_cast and typeid. Compilers can switch it off to save space, and dynamic_cast stops working when they do.

Three hierarchies it cannot handle: a class with no virtual functions is not polymorphic, so the cast is rejected at compile time. Inheritance that is private or protected gives no public base subobject to match. Virtual bases can leave the path ambiguous.

Compared with static_cast: static_cast performs no runtime check, so it is faster and will happily produce a pointer to the wrong type, which is undefined behavior as soon as you use it. Prefer dynamic_cast for downcasts, and keep static_cast for conversions whose correctness is already established.

Type tags are worse than the operator: a virtual function returning an enumerator per class compares equal only for one exact class, so it misclassifies anything derived further. dynamic_cast matches derived classes as well and needs no maintenance.

Design first: a virtual function is the better answer when the behavior belongs to the objects. Downcasting earns its place when the base class is not yours to change, when the thing you need exists on one derived class only, or when no base class implementation would mean anything.