Chapter 13

Polymorphism

Virtual functions, vtables, abstract base classes, pure virtual functions, the diamond problem, and the override/final specifiers.

In this chapter

  1. Virtual Functions
  2. How the vtable Works
  3. Virtual Destructors
  4. Never Call Virtual Methods in Ctors/Dtors
  5. Abstract Base Classes & Pure Virtual
  6. The Diamond Problem & Virtual Inheritance
  7. override & final
  8. Default Parameters & Virtual Functions
  9. OOP Tradeoffs
1

Virtual Functions

The virtual keyword on a method ensures the compiler uses the most-derived overriding version when the function is called through a base pointer or reference. Without it, the base version is always called regardless of the actual object type.

class Fish {
public:
    virtual void Swim();  // virtual — derived can override
};

class Tuna : public Fish {
public:
    void Swim();  // overrides Fish::Swim
};

void Func(Fish& fishy) {
    fishy.Swim();  // calls Tuna::Swim if a Tuna is passed
}

Fish* dinner = new Tuna();
dinner->Swim();  // calls Tuna::Swim — needs pointer for vtable to work
Virtual functions cannot be static — static functions have no this pointer and are resolved at compile time, which is the opposite of what virtual dispatch does.

2

How the vtable Works

The compiler implements virtual dispatch through a virtual function table (vtable). Here's what happens under the hood:

  1. Every class that uses virtual functions (declared or inherited) gets its own vtable — a static array of function pointers, one entry per virtual function, pointing to the most-derived implementation.
  2. A hidden vptr (virtual pointer) is added to every instance of such a class. It points to the class's vtable.
  3. When a virtual function is called through a pointer/reference, the program follows the vptr to the vtable and looks up the right function pointer at runtime.
Object in memory
vptr ──────────→
member data
...
vtable (static)
[0] → Tuna::Swim
[1] → Fish::~Fish
The vptr adds overhead — typically one pointer per object (8 bytes on 64-bit). A class with even one virtual function incurs this cost for every instance. In performance-critical code (e.g., thousands of tiny game entities), prefer non-virtual designs or data-oriented approaches.

3

Virtual Destructors

If you delete a derived object through a base pointer and the base destructor is not virtual, the derived portion may never be destroyed — causing a memory leak or undefined behavior:

Base* b = new Derived();
delete b;  // calls Base::~Base only — Derived's destructor never runs!

Making the base destructor virtual fixes this — the vtable ensures the correct destructor chain is called:

class Base {
public:
    virtual ~Base() {}  // virtual — Derived's dtor will be called first
};
Rule of thumb: if a class has at least one virtual function, make its destructor virtual too. However, adding a virtual destructor to a class that otherwise has no virtual functions adds a vptr to every object — only do it when inheritance is intended.

4

Never Call Virtual Methods in Ctors/Dtors

The vtable is not fully set up during construction or destruction. The base class is constructed before the derived class, so when the base constructor runs, the vptr points to the base vtable — not the derived one. Calling a virtual function there won't dispatch to the derived implementation.

class Base {
public:
    Base() {
        Init();  // calls Base::Init, NOT Derived::Init — even if overridden!
    }
    virtual void Init();
};
This is a common source of bugs. Never rely on virtual dispatch inside a constructor or destructor.

5

Abstract Base Classes & Pure Virtual

A pure virtual function is declared with = 0 and has no implementation in the base class. Any class with at least one pure virtual function becomes an abstract base class (ABC) — it cannot be instantiated directly.

class Shape {
public:
    virtual void Draw() = 0;  // pure virtual — Shape is now abstract
    virtual ~Shape() {}
};

Shape s;      // ERROR — cannot instantiate abstract class
Shape* s;     // OK — pointer to abstract class is fine

Derived classes must implement all pure virtual functions to become concrete (instantiable). ABCs are essentially C++'s way of defining an interface.

An ABC can still have non-pure virtual functions and non-virtual functions. Only its derived children can call them, since the ABC itself cannot be instantiated.

6

The Diamond Problem & Virtual Inheritance

When multiple inheritance creates a shared ancestor, the ancestor is included multiple times — once per path. This is the diamond problem:

class Parent  { public: int age; };
class ChildA  : public Parent {};
class ChildB  : public Parent {};
class ChildC  : public Parent {};
class GrandChild : public ChildA, public ChildB, public ChildC {};
Parent
↙      ↓      ↘
ChildA
ChildB
ChildC
↘     ↓     ↙
GrandChild

GrandChild ends up with 3 copies of Parent — one per path. The constructor for Parent is called 3 times, and gp.age is ambiguous.

Fix: Virtual Inheritance

Mark the inheritance as virtual in the intermediate classes. This ensures only one shared copy of the common base is created:

class ChildA : public virtual Parent {};
class ChildB : public virtual Parent {};
class ChildC : public virtual Parent {};
class GrandChild : public ChildA, public ChildB, public ChildC {
    // Now only one Parent subobject — no ambiguity
};

7

override & final

override

Adding override to a method signals your intent to override a virtual function. The compiler then enforces two checks:

  1. The base class version must be virtual
  2. The signatures must match exactly
class Tuna : public Fish {
public:
    void Swim() override;  // compiler verifies this actually overrides Fish::Swim
};
Always use override on derived virtual functions. It turns silent bugs (wrong signature) into compiler errors.

final on Methods

final can also be applied to a virtual method (not just a class) to prevent further derived classes from overriding it:

class Tuna : public Fish {
public:
    void Swim() override final;  // no class deriving from Tuna can override Swim
};

8

Default Parameters & Virtual Functions

Virtual functions are dynamically bound — the derived implementation is called. But default parameter values are statically bound — they are resolved at compile time based on the declared type of the pointer/reference, not the actual object.

class Base {
public:
    virtual void Log(int x = 10);
};

class Derived : public Base {
public:
    void Log(int x = 99) override;
};

Base* b = new Derived();
b->Log();  // calls Derived::Log — but with x = 10 (Base's default)!
Never redefine a function's inherited default parameter value. The behavior is confusing and counterintuitive. If you need a different default, use a non-virtual wrapper function instead.

9

OOP Tradeoffs

Inheritance — Pros
  • Code reusability
  • Saves time/effort for large families of types
  • Easier to change functionality for groups of objects
  • Can improve readability
Inheritance — Cons
  • Tight coupling between base and derived
  • Difficult to model ambiguous objects
  • Cascading changes — a base change can break all derived classes
Polymorphism — Pros
  • Flexibility and code reusability
  • Reduces coupling between domains (e.g., collider interface)
  • Enables "virtual — using one in place of another"
Polymorphism — Cons
  • Slightly larger objects (vptr overhead)
  • Slight runtime cost of vtable lookup
  • Not immediately obvious what implementation a function call hits
← Chapter 12 ↑ Index Chapter 14 →