C++ Virtual Functions and Polymorphism: Trace Dynamic Dispatch Step by Step
Trace a Shape hierarchy line by line to see how static type, dynamic type, final overriders, object slicing, and virtual destructors determine C++ output.
KnowledgeGate Team
Exam prep & CS education

Reading an inheritance hierarchy feels easy until a Shape* points to a Square and you must predict the output without running the program. Separate the pointer's static type from the object's dynamic type, then find each final overrider. Slicing and destructor dispatch are distinct parts of polymorphic behavior. Coding & DSA Courses for Placements places the topic inside a broader learning route.
C++ dynamic dispatch: the three facts beside every call
Polymorphism lets one base interface produce behaviour from different derived objects. A virtual call through a base pointer or reference uses the dynamic type of the complete object.
Write down three facts before tracing it:
The static type comes from the expression's declaration.
The dynamic type is the actual most-derived object.
The final overrider is the implementation selected.
For this declaration, Shape* p = new Rectangle(3, 4);, the distinction is compact:
expression | static type | dynamic type | candidate selected |
|---|---|---|---|
|
|
|
|
A virtual table is a common implementation model, but C++ does not mandate that object layout. Virtual Functions and VTable in C++ explains that implementation model with a compact two-shape program. The Shape/Rectangle/Square trace below tests a different boundary: the most-derived name changes while a final area override stays inherited, then slicing, qualification, and deletion change dispatch.
C++ virtual function hierarchy: build the Shape example
The Shape hierarchy is:
#include <iomanip>
#include <iostream>
#include <string>
struct Shape {
virtual double area() const { return 0.0; }
virtual std::string name() const { return "Shape"; }
virtual ~Shape() { std::cout << "~Shape\n"; }
};
struct Rectangle : Shape {
double width, height;
Rectangle(double w, double h) : width(w), height(h) {}
double area() const override final { return width * height; }
std::string name() const override { return "Rectangle"; }
~Rectangle() override { std::cout << "~Rectangle\n"; }
};
struct Square : Rectangle {
explicit Square(double side) : Rectangle(side, side) {}
std::string name() const override { return "Square"; }
~Square() override { std::cout << "~Square\n"; }
};
struct Circle final : Shape {
double radius;
explicit Circle(double r) : radius(r) {}
double area() const override { return 3.14 * radius * radius; }
std::string name() const override { return "Circle"; }
~Circle() override { std::cout << "~Circle\n"; }
};virtual creates the dispatch contract in Shape. override makes the compiler verify the signature. final on Rectangle::area() prevents another override, while final on Circle prevents further derivation. Square may inherit Rectangle::area(), but may not replace it.
The C++ Programming Course provides a structured route through C++ concepts, MCQs, and coding practice.
Trace dynamic dispatch through three Shape pointers
The driver fixes two decimal places and calls three dynamic objects through one pointer type:
int main() {
std::cout << std::fixed << std::setprecision(2);
Shape* shapes[] = {
new Rectangle(3, 4),
new Square(5),
new Circle(2)
};
for (Shape* p : shapes) {
std::cout << p->name() << ": " << p->area() << '\n';
}
for (Shape* p : shapes) {
delete p;
}
}Trace each call before looking at the output:
Rectangle(3, 4):name()selectsRectangle::name(). The final overrider forarea()isRectangle::area(), and3 * 4 = 12.00.Square(5):name()selectsSquare::name(). The stored dimensions are5and5.area()is inherited fromRectangle, so5 * 5 = 25.00.Circle(2): both calls selectCircleimplementations. Using3.14,3.14 * 2 * 2 = 12.56.
The loop prints exactly these three lines:
Rectangle: 12.00
Square: 25.00
Circle: 12.56The static pointer type remains Shape* in every iteration. Only the dynamic object changes.

C++ override and final: make wrong hierarchies fail
Suppose a derived class contains this function:
double area() override { return 1.0; }It lacks the trailing const from Shape::area() const, so it is not an override. The specifier turns a silent new function into a compiler diagnostic.
Two different uses of final also reject these declarations:
struct BadSquare : Rectangle {
double area() const override { return 99.0; }
};
struct SpecialCircle : Circle {};BadSquare::area() tries to replace a final function. SpecialCircle tries to derive from a final class. Both are ill-formed.
Overriding is not overloading. Parameters and cv/ref qualifiers, including const, must satisfy the override rules. A return type alone cannot create an overload, and a derived same-named declaration can hide base overloads. Add override to every intended override. Use final only where the hierarchy contract ends.
C++ polymorphism traps: slicing and qualified calls
Start with a reference:
Rectangle rect(3, 4);
Shape& ref = rect;ref.area() selects Rectangle::area() and gives 12.00 because the reference preserves the complete Rectangle object.
Now copy into a base object: Shape sliced = rect;. It contains only the copied Shape subobject, so sliced.area() returns 0.00. Slicing changes the object, not merely its declaration.
ref.Shape::area() also returns 0.00. Qualification suppresses dispatch and selects the base implementation. During a base constructor or destructor, a virtual call reaches only the class currently being constructed or destroyed, not a more-derived override.
Virtual destructors: trace deletion through Shape*
The driver deletes through Shape* in array order. Since Shape::~Shape() is virtual, destruction begins at each dynamic type and proceeds through its bases:
Rectangle(3, 4)prints~Rectangle, then~Shape.Square(5)prints~Square, then~Rectangle, then~Shape.Circle(2)prints~Circle, then~Shape.

If a base class is intended for polymorphic deletion, its destructor must be virtual. Deleting a derived object through a base pointer with a non-virtual destructor has undefined behaviour. It is not safe to reduce that fault to a predictable skipped destructor or leak.
How interviews and exams test C++ virtual functions
Use this five-step checklist for output questions:
Mark every virtual declaration.
Preserve exact signatures, including
constand ref qualifiers.Write the expression's static type and the object's dynamic type.
Find the final overrider.
Check for slicing or explicit qualification before computing output.
Typical questions ask for the three area values, a compile-time error from overriding a final function or omitting const, or whether deletion through Shape* is safe. A non-virtual same-named function is selected from the expression's static type, not by dynamic dispatch.
For the wider relationship among encapsulation, inheritance, and polymorphism, continue with Object Oriented Technology Explained.
C++ dynamic dispatch: the short version and next step
Virtual calls use the dynamic object. override verifies intent, final closes an override or class, slicing removes the derived part, qualification suppresses dispatch, and a polymorphic base needs a virtual destructor.
Add a LabelledSquare derived from Square. Override name() to return "LabelledSquare" and give its destructor one print line, but do not override the final Rectangle::area(). With side 6, a Shape* prints LabelledSquare: 36.00; deletion prints ~LabelledSquare, ~Square, ~Rectangle, ~Shape. The same object therefore has two final overriders: LabelledSquare::name() and inherited Rectangle::area().
For C++, other languages, competitive coding, and placement-oriented practice in one route, continue with Coding for Placements.
Keep learning

C++ Abstract Classes and Interfaces: Build a Usable Storage Contract
Build a C++17 ScoreStore interface, swap vector and hash-backed implementations behind it, and verify both with the same runnable contract test.

C++ unordered_map and unordered_set: Hashing, Equality and a Custom-Key Frequency Counter
Build a correct mental model for C++ unordered containers, then trace a custom-key counter through a real collision without merging distinct keys.

C++ STL Containers: Choose by Access Pattern, Mutation Cost and Ownership
Choose a C++ STL container from the workload, not from habit. Trace sequence operations, keyed counts, invalidation and object lifetime through exact examples.

C++ STL Algorithms: Sort, Search, Transform and Clean Data in One Pipeline
Trace nine sensor readings through sort, lower_bound, transform, erase-remove and accumulate, with every iterator rule and intermediate value explained.