There is a huge difference between static_cast and dynamic_cast, I'll just reduce the discussion to the objects world.
A static_cast may be used for the (infamous) up-cast. That is:
void foo(Base& b) { Derived& d = static_cast<Derived&>(b); }
The compiler can assess whether this is legal or not, because having the definition of Derived it knows whether or not Derived is actually a descendent of Base.
For non-trivial hierarchies:
struct Base {}; struct D1: Base {}; struct D2: Base {}; struct Derived: D1, D2 {};
This would yield an error: the compiler would not know from which of the bases (the one from D1 or the one from D2 you came from).
Or if you used virtual inheritance:
struct VDerived: virtual VBase {};
the compiler would need run-time information, and thus the compilation would fail too.
A dynamic_cast however is much more clever, though this comes at the cost of runtime-overhead. The compiler generates information about the objects generally know as RTTI (RunTime Type Information), that the dynamic_cast will explore to allow:
Cast depending on the true runtime type:
// will throw `bad_cast` if b is not a `Derived` void foo(Base& b) { Derived& d = dynamic_cast<Derived&>(b); }
Cast accross branches:
struct X {}; struct Y {}; void foo(X* x) { Y* y = dynamic_cast<Y*>(x); } // If I add: struct Z: X, Y {}; // then if x is in fact a Z, y will be non-null
Cast when there is virtual inheritance involved.
void bar(VBase* vb) { VDerived* vd = dynamic_cast<VDerived*>(vb); }
Cast to void* to get the true address of the object (this is somewhat special).
void foobar(X* x) { void* xAddr = dynamic_cast<void*>(x); Y* y = dynamic_cast<Y*>(y); void* yAddr = dynamic_cast<void*>(y); Z* z = dynamic_cast<Z*>(x); void* zAddr = dynamic_cast<void*>(z); if (z) { assert(xAddr == yAddr && xAddr == zAddr); } }
Note that this still doesn't work if there is an ambiguity because of the hierarchy (as with the D1/D2 example above).