C++ / INHERITANCE AND POLYMORPHISM
Virtual functions and runtime dispatch
Call the right override through a base reference or pointer, and predict which body runs by separating static type from dynamic type.
What you will learn
- Mark a base member virtual so calls through Base& or Base* run the derived body
- Predict a call by separating the expression's static type from the object's dynamic type
- Call Base::f() inside an override to reuse base behaviour without infinite recursion
- Know that default arguments come from the static type, not the override that runs
Understanding Virtual functions and runtime dispatch
Every expression in C++ has a static type, which is what the compiler reads in the source, and every object has a dynamic type, which is what it actually is at run time. If you write Shape& s = sq;, the static type of s is Shape while the dynamic type of the object it refers to is Square. A call to a non-virtual member function is compiled as a direct call to whatever the static type provides, so it cannot notice the difference. Marking the function virtual replaces that rule with a different one: ask the object, and run the most derived override of that function.
The two types can only disagree where there is indirection, so dispatch is a property of references and pointers. A variable of type Shape holds a Shape object, its dynamic type equals its static type, and there is nothing left to select. The same reasoning explains why a virtual call made from inside a base member function reaches the derived body: this is a Shape* aimed at the whole Square object, so describe() written in Shape ends up executing Square::area, code that did not exist when Shape was compiled. That is what lets a base class define the shape of an algorithm and leave holes for derived classes to fill.
Only the choice of body is deferred. Name lookup, overload resolution, the access check, and default arguments are all settled at compile time from the static type, which is why an override must repeat the base signature exactly, why hiding a non-virtual function gives you no dispatch at all, and why default arguments in an override are ignored when you call through a base reference. Writing a qualified call such as Base::f() names one body directly and turns dispatch off. The short version of the model: the compiler decides which function is called, the object decides whose body runs.
<iostream>
<string>
struct Shape {
virtual double area() const { return 0.0; }
std::string label() const { return "shape"; } // not virtual
void describe() const { // not virtual
std::cout << label() << " area=" << area() << "\n";
}
virtual ~Shape() = default;
};
struct Square : Shape {
double side;
explicit Square(double s) : side(s) {}
double area() const override { return side * side; }
std::string label() const { return "square"; } // hides Shape::label
};
int main() {
Square sq(3.0);
Shape& s = sq;
std::cout << s.area() << "\n"; // virtual: dynamic type decides
std::cout << s.label() << "\n"; // non-virtual: static type decides
std::cout << sq.label() << "\n"; // static type here is Square
s.describe(); // base code, derived area
}
virtual defers only the choice of function body to the object's dynamic type; everything else about the call is fixed at compile time by the static type.
Worked examples
One call site, several behaviours
A loop over base pointers runs a different body per element without any branching in the loop.
<iostream>
<vector>
struct Animal {
virtual const char* sound() const { return "..."; }
virtual ~Animal() = default;
};
struct Dog : Animal {
const char* sound() const override { return "woof"; }
};
struct Cow : Animal {
const char* sound() const override { return "moo"; }
};
int main() {
Dog d;
Cow c;
Animal a;
std::vector<const Animal*> pen{&d, &c, &a};
for (const Animal* p : pen)
std::cout << p->sound() << "\n";
}
Example explained
Line 1sound() is virtual, so p->sound() consults the object p points at rather than the pointer's type const Animal*.
Line 2Dog::sound and Cow::sound repeat the base signature exactly, including const, so they override instead of hiding.
Line 3The loop contains a single call site; adding a Sheep class changes the output without editing that line.
Line 4Animal a is in the vector on purpose: its dynamic type is Animal, so the base fallback "..." runs.
Turning dispatch off with a qualified call
Base::f() names one specific body, which is how an override extends base behaviour instead of recursing into itself.
<iostream>
struct Logger {
virtual void write(const char* msg) const {
std::cout << "[base] " << msg << "\n";
}
virtual ~Logger() = default;
};
struct TimestampLogger : Logger {
void write(const char* msg) const override {
std::cout << "12:00 ";
Logger::write(msg);
}
};
void emit(const Logger& lg) { lg.write("started"); }
int main() {
TimestampLogger t;
emit(t);
Logger& ref = t;
ref.Logger::write("forced base");
}
Example explained
Line 1emit takes const Logger&, so lg.write dispatches on the dynamic type and TimestampLogger::write runs.
Line 2An unqualified write(msg) inside that override would dispatch back to itself and recurse until the stack dies.
Line 3Logger::write(msg) is a qualified call, which suppresses dispatch and calls exactly that body.
Line 4ref.Logger::write(...) does the same through a base reference, skipping the timestamp even though ref refers to a TimestampLogger.
Default arguments stay static
The body comes from the dynamic type but the omitted argument comes from the declaration the compiler can see.
<iostream>
struct Base {
virtual void show(int n = 1) const {
std::cout << "Base::show " << n << "\n";
}
virtual ~Base() = default;
};
struct Derived : Base {
void show(int n = 99) const override {
std::cout << "Derived::show " << n << "\n";
}
};
int main() {
Derived d;
Base& b = d;
b.show();
d.show();
}
Example explained
Line 1b.show() dispatches to Derived::show because show is virtual and b refers to a Derived object.
Line 2The value 1 is filled in from Base::show, since the missing argument is supplied at compile time from the static type Base.
Line 3d.show() reaches the same body, but the static type is Derived, so 99 is used instead.
Line 4The safe habit is to give the override no default at all, so there is only one value to reason about.
Important notes
A derived function that matches the base signature is virtual whether or not you write the keyword again, so dispatch keeps working through a third or fourth layer of derivation.
Virtualness belongs to individual functions, not to classes: within the same object one call can reach a derived override while the next line, calling a non-virtual member, reaches the base version.
Common mistakes
Leaving virtual off the base declaration: Base& b = derived; b.f(); still compiles, but the call is resolved statically and Base::f runs, so the override is silently dead code.
Calling a virtual function from a base constructor or destructor: while Base's constructor runs the object's dynamic type is still Base, so you get Base::f, and if it is pure virtual the behaviour is undefined.
Writing a parameter as Base instead of const Base& or Base*: the function receives a Base copy whose dynamic type is Base, so every virtual call inside picks the base body.
Try it yourself
Change, predict, then run
Write a Notifier base with a virtual send(const std::string&) plus Email and Sms derived classes, put one of each in a std::vector<Notifier*> and send through a single loop. Then delete the word virtual, rerun, and explain the new output in one sentence.
Open the C++ workspaceCheck your understanding
struct A { virtual void f() const { std::cout << "A"; } void g() const { f(); } }; struct B : A { void f() const override { std::cout << "B"; } }; B b; const A& r = b; r.g(); What is printed, and why?
- B, because A::g is the body that runs but the f() inside it is resolved on the object's dynamic type
- A, because g is not virtual, so calls written inside it resolve to A's own members
- AB, because the call reaches A::f and then B::f
- Nothing, because g() cannot be called through a const A& once B overrides f
Show answer
g is not virtual, so the body executed really is A::g, which is the half-truth that makes option 1 tempting. But inside that body this points at a B object, and f is virtual, so f() is dispatched on the dynamic type and B::f prints B. Being virtual is a property of each function, not of the class or of the file the call happens to be written in.