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

Updated 7 Oct 20265 min read

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:

  1. The static type comes from the expression's declaration.

  2. The dynamic type is the actual most-derived object.

  3. 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

p->area()

Shape* for p

Rectangle

Rectangle::area()

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:

cpp
#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:

cpp
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:

  1. Rectangle(3, 4): name() selects Rectangle::name(). The final overrider for area() is Rectangle::area(), and 3 * 4 = 12.00.

  2. Square(5): name() selects Square::name(). The stored dimensions are 5 and 5. area() is inherited from Rectangle, so 5 * 5 = 25.00.

  3. Circle(2): both calls select Circle implementations. Using 3.14, 3.14 * 2 * 2 = 12.56.

The loop prints exactly these three lines:

Code
Rectangle: 12.00
Square: 25.00
Circle: 12.56

The static pointer type remains Shape* in every iteration. Only the dynamic object changes.

Three Shape pointers dispatching to Rectangle, Square, and Circle, with printed outputs 12.00, 25.00, and 12.56.

C++ override and final: make wrong hierarchies fail

Suppose a derived class contains this function:

cpp
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:

cpp
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:

cpp
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:

  1. Rectangle(3, 4) prints ~Rectangle, then ~Shape.

  2. Square(5) prints ~Square, then ~Rectangle, then ~Shape.

  3. Circle(2) prints ~Circle, then ~Shape.

Destruction order when a Square is deleted through a Shape pointer: ~Square, then ~Rectangle, 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:

  1. Mark every virtual declaration.

  2. Preserve exact signatures, including const and ref qualifiers.

  3. Write the expression's static type and the object's dynamic type.

  4. Find the final overrider.

  5. 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.