Today I Learned that static_cast can perform an unchecked downcast. It is safe only when we can prove that a base pointer refers to the requested derived type; otherwise, the program has undefined behavior.

We need a simple class hierarchy:

  • Base class B
  • Derived class D0 : B
  • Derived class D1 : B

D0 has one int data member, while D1 has one double data member.

// This example requires C++11 or later.
struct B
{
    virtual ~B() = default; // Makes B polymorphic, so dynamic_cast can check the runtime type.
};
struct D0 : B
{
    int i{};
};
struct D1 : B
{
    double d{};
};

// Helper functions to visualise the contents of the objects.
void f(const D0 *d)
{
    std::cout << "f(D0*) = " << d->i << '\n';
}
void f(const D1 *d)
{
    std::cout << "f(D1*) = " << d->d << '\n';
}

Inside main, we create an object d0 of type D0. We can create a B* pointer that points to its B base-class subobject with an implicit, safe derived-to-base conversion. An explicit static_cast is unnecessary here.

    D0 d0;

    // D1 *d1 = static_cast<D1 *>(&d0); // Does not compile: D0 and D1 are sibling types.
    B *b = &d0; // Safe, implicit derived-to-base conversion.

Although both D0 and D1 publicly derive from B, b actually points to the B base-class subobject of a D0 object. Therefore, the downcast to D0* is defined.

    // f(*b); // Ill-formed: *b is a B&, but the overloads expect D0* or D1*.
    f(static_cast<D0*>(b)); // Defined: b refers to the B subobject of d0.

The following expression is well-formed because D1 derives from B, but it must not be executed in this example:

    // f(static_cast<D1 *>(b)); // Undefined behavior: b does not point to a D1 object.

static_cast does not inspect the runtime type and does not reinterpret a D0 object as a D1 object. For a base-to-derived pointer conversion, the C++ standard requires b to point to the B base-class subobject of an actual D1 object. Since it points to a D0 object instead, evaluating the cast has undefined behavior. The standard does not guarantee an output, a memory layout, or a compiler diagnostic.

When the dynamic type is uncertain, use dynamic_cast. Because B is polymorphic, this checked downcast returns nullptr when b does not point to a D1 object.

    if (auto *d1 = dynamic_cast<D1*>(b))
    {
        f(d1);
    }
    else
    {
        std::cout << "b does not point to a D1 object\n";
    }

After changing the value in d0, the valid downcast still produces the expected result:

    d0.i = 42;
    f(static_cast<D0*>(b)); // Defined.
f(D0*) = 42

static_cast is useful when the program can prove the dynamic type. Otherwise, prefer dynamic_cast or redesign the interface to avoid the downcast.

“With great power comes great responsibility.” - Spider-Man