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