C++InheritancePolymorphismvtableVirtual Destructor

Inheritance and Virtual Dispatch

Module 3 of 825 min readLevel: Medium

Setup

Quant libraries are built around families of related models. A Monte Carlo path simulator does not care whether the underlying follows Black-Scholes geometric Brownian motion, Dupire's local volatility model, or Heston's stochastic volatility process — it only needs to call drift(t, S) and diffusion(t, S) on whatever model it holds. This is the canonical motivation for inheritance and polymorphism in quantitative finance.

The mechanism that makes this work is virtual dispatch: the decision of which function body to execute is deferred from compile time to runtime, based on the dynamic (actual) type of the object, not the static (declared) type of the pointer.

This module follows the running example from the Gustave Eiffel M2 course: a Matrix base class and a SquareMatrix derived class. The same structural patterns transfer directly to Model/BlackScholesModel, Payoff/CallPayoff, and Distribution/NormalDistribution.

Assumptions and prerequisites: C++ basic class syntax, the Rule of Five, move semantics (Module 2). This module targets C++17; override and = default are C++11 features and are mandatory.


Theory

1. Inheritance Syntax and the "is-a" Relationship

class SquareMatrix : public Matrix {
    // ...
};

public inheritance establishes the is-a relationship: a SquareMatrix is a Matrix. Anywhere a Matrix& or Matrix* is expected, a SquareMatrix can be used — this is the Liskov Substitution Principle.

Access rules under public inheritance:

  • public members of Matrix remain public in SquareMatrix.
  • protected members of Matrix are accessible inside SquareMatrix member functions.
  • private members of Matrix exist in the SquareMatrix object but are inaccessible from SquareMatrix code. They are only accessible via Matrix's own member functions.

2. Constructor Delegation

A derived class constructor must initialise the base sub-object via the member-initialiser list. The base is always constructed before the derived members.

// SquareMatrix stores only dimension; data lives in Matrix
SquareMatrix::SquareMatrix(size_t n, const std::vector<Vector>& data)
    : Matrix(n, n, data)   // base constructed first
{}

If no base constructor is listed, the compiler calls the base's default constructor. If the base has no default constructor, this is a compile error.

3. Copy Constructor in a Derived Class

The copy constructor of a derived class must explicitly invoke the base copy constructor. Failure to do so calls the base's default constructor, leaving the base sub-object default-initialised rather than copied.

SquareMatrix::SquareMatrix(const SquareMatrix& m)
    : Matrix(m)   // binds to Matrix::Matrix(const Matrix&) via implicit upcast
{}

m is a SquareMatrix const&, which binds to Matrix const& implicitly — no cast required.

4. Move Constructor in a Derived Class

SquareMatrix::SquareMatrix(SquareMatrix&& m) noexcept
    : Matrix(std::move(m))   // cast m to Matrix&&, invokes Matrix's move constructor
{}

std::move(m) casts m from SquareMatrix&& to SquareMatrix&& (a no-op cast), but when this is passed to Matrix::Matrix(Matrix&&), the implicit conversion to Matrix&& occurs. This correctly invokes the base move constructor, which steals the base sub-object's resources. The SquareMatrix-specific members are then move-constructed in declaration order.

5. Copy Assignment in a Derived Class

SquareMatrix& SquareMatrix::operator=(const SquareMatrix& m) {
    Matrix::operator=(m);   // explicit call to base assignment
    // ... copy SquareMatrix-specific members ...
    return *this;
}

Using Matrix::operator=(m) explicitly calls the base assignment. Without this, base members would not be updated.

6. Move Assignment in a Derived Class

SquareMatrix& SquareMatrix::operator=(SquareMatrix&& m) noexcept {
    Matrix::operator=(std::move(m));   // steal base resources
    // ... move SquareMatrix-specific members ...
    return *this;
}

7. The Virtual Dispatch Problem

Consider:

Matrix* p = new SquareMatrix(3, data);
std::string s = p->name();   // which name() is called?

Without virtual, C++ uses the static type of p — which is Matrix* — to resolve the call at compile time. Matrix::name() is always called, regardless of what object p actually points to. This breaks polymorphism: the whole point of holding a Matrix* to a SquareMatrix is that the SquareMatrix behaviour should activate.

8. The virtual Keyword and the vtable

Declaring a member function virtual in the base class instructs the compiler to use runtime dispatch:

class Matrix {
public:
    virtual std::string name() const;
    // ...
};

The implementation:

  • Each class with at least one virtual function gets a vtable (virtual dispatch table): a static, per-class array of function pointers, one per virtual method, set up at compile time.
  • Each object of a polymorphic class carries a hidden vptr (virtual pointer): a pointer to its class's vtable, set by the constructor.
  • A virtual call p->name() at runtime: dereference p to get the vptr, index the vtable at the slot for name, call through the resulting function pointer.

Cost: one pointer dereference per virtual call plus the vptr storage overhead (one pointer per object, typically 8 bytes on 64-bit). For most quant workloads this is negligible. In tight inner loops (e.g., calling diffusion() for every path step across 10^6 paths), measure before assuming it matters.

The sizeof a polymorphic object includes the vptr:

struct Plain  { double x; };           // sizeof == 8
struct Poly   { virtual void f(); double x; };  // sizeof == 16 (8 for vptr + 8 for x, typical)

9. The override Specifier (C++11, Mandatory)

class SquareMatrix : public Matrix {
public:
    std::string name() const override;   // compiler verifies this overrides a virtual
};

override causes a compile-time error if the method does not actually override a virtual in a base class. Without override, a typo silently introduces a new, unrelated method:

// Without override: compiles silently, but does NOT override Matrix::name() const
std::string Name() const;       // different name — new method
std::string name();             // missing const — new overload, not an override

With override: both of the above are compile errors. Always use override.

10. Virtual Destructor — the Mandatory Rule

Matrix* p = new SquareMatrix(3, data);
delete p;

Without virtual ~Matrix():

  • delete p uses the static type of pMatrix* — to find the destructor.
  • Only ~Matrix() is called.
  • ~SquareMatrix() is never called.
  • Any resources owned by SquareMatrix (heap memory, file handles, etc.) are leaked.
  • This is undefined behaviour per the C++ standard.

Fix:

class Matrix {
public:
    virtual ~Matrix() = default;
    // ...
};

The rule: any class that is used as a polymorphic base (i.e., any class whose destructor may be invoked through a pointer or reference to a base class) must declare its destructor virtual. No exception.

= default generates the correct defaulted destructor while making it virtual. Use this unless the base destructor needs custom logic.


Implementation

Matrix.h

#pragma once
#include <vector>
#include <string>

// A dense matrix stored as a vector of row-vectors.
// Used as a polymorphic base; destructor is virtual.
class Vector;  // forward declaration

class Matrix {
public:
    // --- Construction ---
    Matrix(size_t rows, size_t cols, const std::vector<std::vector<double>>& data);
    Matrix(size_t rows, size_t cols);  // zero-initialised

The full lesson requires Premium

The complete derivation, the C++ / Python implementation, the validation tables, the quiz, and the interview-angle notes are part of Premium. Start a Premium plan to unlock every module in this track.