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. The complete example also needs the standard iostream header for std::cout.
// 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 intentionally has undefined behavior in this example. Do not use this pattern in production code; it is included here to demonstrate how undefined behavior can look harmless in one particular run.
f(static_cast<D1 *>(b)); // Undefined behavior: b does not point to a D1 object.
On one implementation, the two calls might appear to produce the following output:
f(D0*) = 0
f(D1*) = 0
This apparent success is not meaningful. 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.
Changing d0.i can make the result look even more surprising:
d0.i = 42;
f(static_cast<D0*>(b)); // Defined.
f(static_cast<D1 *>(b)); // Still undefined behavior.
For example, one particular run might print:
f(D0*) = 42
f(D1*) = 2.07508e-322
This does not mean that 42 is represented as the double value 2.07508e-322. After undefined behavior occurs, any result is possible: the program may print a different value, appear to work, crash, or behave differently after an unrelated code change or a compiler-option change. Object layout, padding bytes, endianness, optimization, and the implementation can all affect what we happen to observe before the effects of undefined behavior become visible.
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";
}static_cast is useful when the program can prove the dynamic type. Otherwise, prefer dynamic_cast or redesign the interface to avoid the downcast.
An open question
This invalid downcast is undefined behavior, and the C++ standard does not require a compiler to diagnose it. Should this kind of incorrect static_cast remain undefined behavior, or should the language give it defined failure semantics? At the very least, when a compiler or static-analysis tool can prove that a static_cast downcast cannot match the object’s dynamic type, should it issue a warning? A diagnostic cannot be guaranteed in every case because the dynamic type is often known only at runtime, but this example is simple enough that a warning would be useful.
“With great power comes great responsibility.” - Spider-Man