static_cast: Upcasting, Downcasting, and Undefined Behavior
April 23, 2026 · Luciano Muratore
static_cast: Upcasting, Downcasting, and Undefined Behavior
static_cast is C++‘s way of performing explicit type conversions at compile time. It can convert between numeric types, navigate class hierarchies upward and downward, and more—all without any runtime overhead. But with that power comes a responsibility: the compiler trusts you. And when that trust is misplaced, the behavior is undefined.
The Code
#include <iostream>
using namespace std;
class Base {
public:
virtual void speak() { cout << "Base"; }
};
class Derived : public Base {
public:
void speak() override { cout << "Derived\n"; }
void extra() { cout << "Extra\n"; }
};
int main() {
int x = 42;
// from int to double
double y = static_cast<double>(x);
cout << "Double value: " << y << endl;
// Upcasting: Derived* to Base*
Derived d;
Base* basePtr = static_cast<Base*>(&d);
basePtr->speak(); // Calls Derived::speak() because it's virtual
// Downcasting: Base* to Derived* (safe — base actually points to a Derived)
Base* base = new Derived();
Derived* derivedPtr = static_cast<Derived*>(base);
derivedPtr->speak();
// Downcasting: Base* to Derived* (unsafe — base actually points to a Base)
Base b;
Base* wrongBase = &b;
Derived* wrongCast = static_cast<Derived*>(wrongBase);
wrongCast->speak();
}
The output is:
Double value: 42
Derived
Derived
Base
Numeric Conversion
The simplest use of static_cast is converting between compatible numeric types:
double y = static_cast<double>(x);
x is an int. The cast widens it to double. No data loss, no surprises. This is the most straightforward thing static_cast does.
Upcasting: Derived* to Base*
Derived d;
Base* basePtr = static_cast<Base*>(&d);
basePtr->speak(); // Derived
Upcasting—going from a derived class pointer to a base class pointer—is always safe. A Derived object is a Base object. The pointer simply views the object through a narrower lens.
Because speak is virtual, the call resolves to Derived::speak at runtime via the vtable. The static type of the pointer is Base*, but the dynamic type of the object is Derived—and virtual dispatch respects the dynamic type.
Downcasting: Base* to Derived* (Safe)
Base* base = new Derived();
Derived* derivedPtr = static_cast<Derived*>(base);
derivedPtr->speak(); // Derived
Here, base points to an object that is genuinely a Derived. The downcast is telling the compiler: “trust me, I know what this pointer actually points to.” And in this case, that trust is warranted. The object’s memory layout includes everything a Derived object needs, so accessing it as Derived* is correct.
Downcasting: Base* to Derived* (Unsafe)
Base b;
Base* wrongBase = &b;
Derived* wrongCast = static_cast<Derived*>(wrongBase);
wrongCast->speak(); // Base
This is where things get interesting—and dangerous. wrongBase points to a plain Base object. It is not a Derived. Yet static_cast allows the cast without complaint. The compiler does no runtime check; it takes your word for it.
The call wrongCast->speak() outputs "Base". This might seem benign, but the reason it does not crash here is subtle: speak is virtual, so it dispatches through the vptr. The object’s vptr points to Base’s vtable, so Base::speak is called. The output is correct only by coincidence—the virtual dispatch happens to save us.
If wrongCast->extra() were called instead, the result would be undefined behavior: extra is not part of Base, and the object does not have the memory layout that Derived::extra would expect.
Why Does wrongCast->speak() Print "Base"?
This was surprising to me too. The answer is in the vptr.
Every object with virtual functions carries a hidden pointer—the vptr—that points to its class’s vtable. When b is constructed as a Base, its vptr is set to point to Base’s vtable. static_cast does not change the object. It only changes how the pointer sees it. So when wrongCast->speak() is called, the vtable lookup still finds Base::speak, because that is what the object’s vptr points to.
In other words: the cast changed the pointer’s type, not the object’s identity. The object always knew it was a Base.
Summary
static_castperforms compile-time type conversions with no runtime overhead and no safety checks.- Numeric conversions are straightforward and safe when types are compatible.
- Upcasting (Derived* to Base*) is always safe—a derived object is a base object.
- Downcasting (Base* to Derived*) is safe only when the object is genuinely of the derived type.
static_castdoes not verify this. - Casting a
Base*that points to a realBaseobject intoDerived*is undefined behavior—it just happens to print"Base"here because virtual dispatch reads the vptr, which still points toBase’s vtable. - Final Insight:
static_casttrusts the programmer completely. When that trust is misplaced—when the object is not what you claim it is—the compiler will not warn you. The vptr might save you in some cases, but undefined behavior is undefined: you cannot rely on it.