Static and Dynamic Binding
Lesson outline
Two Moments a Call Can Be Decided
Every call to a member function is resolved at one of two moments. Either the COMPILER decides which function runs, by looking at the declared type of the thing you called it on - that is STATIC BINDING, also called early binding. Or the decision is left to the running program, which looks at what the object actually is - that is DYNAMIC BINDING, also called late binding. Everything else in this topic follows from which of the two you got.
That program prints Shape, and the first time you meet it that is a surprise. name() is not virtual, so the compiler resolved the call from the type it could see - Shape* - and never asked what the object really was. The object being a Circle was information nobody consulted.
Static Binding
Static binding uses the STATIC TYPE: the type written in the code. It covers more than you might expect - every non-virtual member call, every free function, every choice between overloads, and every template instantiation is resolved before the program runs. It is the default, and it is the fast case: the compiler knows the exact function, so it can inline it.
Dynamic Binding
Mark the function virtual and the decision moves to runtime. The object carries a hidden pointer to a table of function addresses - the vtable - and the call goes through it, so the version that runs is the one belonging to the object's DYNAMIC TYPE: what it actually is, not what the pointer claims. One keyword changes the output of the previous program.
The loop does not know how many kinds of shape exist, and it does not need to. That is the whole point of dynamic binding: code written once against the base type keeps working when a new derived type is added, because the choice is made by the object rather than by the caller.
The last two columns are what the two programs above actually print. The rows where they disagree are the rows the one keyword changed.
What Actually Switches Dynamic Binding On
Two conditions, and you need BOTH. The function must be declared virtual in the base class, and the call must go through a POINTER or a REFERENCE to the base. Call a virtual function on a plain value and the type is known exactly, so the compiler binds it statically - there is nothing to decide.
| How you call it | Binding | Why |
|---|---|---|
Shape s; s.name(); | static | the type is known exactly - nothing to decide |
Shape* p = &c; p->name(); | dynamic, if virtual | the pointer could address any derived type |
void f(Shape& s) then s.name() | dynamic, if virtual | a reference binds to the real object |
void f(Shape s) then s.name() | static, always | the argument was sliced into a Shape |
Shape::name() written explicitly | static, always | you named the version yourself |
Object Slicing: Dynamic Binding You Never Get
Passing a derived object to a parameter taken BY VALUE copies only the base part of it. The derived half is not converted or hidden, it is discarded - the parameter is a genuine Shape and there is nothing left to dispatch to. This is called slicing, and it is the most common way a correct-looking virtual function fails to run.
Why the Destructor Has to Be Virtual
Deleting a derived object through a base pointer is the same question one more time: which destructor runs? If the base destructor is not virtual the answer is decided statically, only the base part is destroyed, and whatever the derived class owned is leaked. The standard calls this undefined behaviour, which in practice means it usually looks fine until it does not. A class meant to be inherited from gets a virtual destructor.
What Dynamic Binding Costs
One extra indirection per call - read the vtable pointer, then the function address, then jump - and, more importantly, the call cannot be inlined, because the compiler does not know which function it is. For most code that is irrelevant. It matters in the innermost loop of something like a sort, which is exactly why a comparator is usually passed as a TEMPLATE parameter rather than as a base-class pointer: a template argument is a type known at compile time, so the call is statically bound and can be inlined away entirely.