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

    // --- Rule of Five ---
    Matrix(const Matrix& m);
    Matrix(Matrix&& m) noexcept;
    Matrix& operator=(const Matrix& m);
    Matrix& operator=(Matrix&& m) noexcept;
    virtual ~Matrix() = default;   // MANDATORY: virtual destructor

    // --- Virtual interface ---
    virtual std::string name() const;  // returns "Matrix"

    // --- Accessors ---
    size_t rows() const { return _rows; }
    size_t cols() const { return _cols; }
    double operator()(size_t i, size_t j) const;
    double& operator()(size_t i, size_t j);

protected:
    size_t _rows;
    size_t _cols;
    std::vector<std::vector<double>> _data;  // _data[i][j]: row i, column j
};

Matrix.cpp

#include "Matrix.h"
#include <stdexcept>

Matrix::Matrix(size_t rows, size_t cols,
               const std::vector<std::vector<double>>& data)
    : _rows(rows), _cols(cols), _data(data) {
    if (data.size() != rows)
        throw std::invalid_argument("Matrix: row count mismatch");
    for (const auto& row : data)
        if (row.size() != cols)
            throw std::invalid_argument("Matrix: column count mismatch");
}

Matrix::Matrix(size_t rows, size_t cols)
    : _rows(rows), _cols(cols),
      _data(rows, std::vector<double>(cols, 0.0)) {}

Matrix::Matrix(const Matrix& m)
    : _rows(m._rows), _cols(m._cols), _data(m._data) {}

Matrix::Matrix(Matrix&& m) noexcept
    : _rows(m._rows), _cols(m._cols), _data(std::move(m._data)) {
    m._rows = 0;
    m._cols = 0;
}

Matrix& Matrix::operator=(const Matrix& m) {
    if (this != &m) {
        _rows = m._rows;
        _cols = m._cols;
        _data = m._data;
    }
    return *this;
}

Matrix& Matrix::operator=(Matrix&& m) noexcept {
    if (this != &m) {
        _rows = m._rows;
        _cols = m._cols;
        _data = std::move(m._data);
        m._rows = 0;
        m._cols = 0;
    }
    return *this;
}

std::string Matrix::name() const { return "Matrix"; }

double Matrix::operator()(size_t i, size_t j) const {
    return _data.at(i).at(j);
}

double& Matrix::operator()(size_t i, size_t j) {
    return _data.at(i).at(j);
}

SquareMatrix.h

#pragma once
#include "Matrix.h"

// A square matrix (n x n). Inherits storage from Matrix.
// Adds a virtual name() override demonstrating polymorphic dispatch.
class SquareMatrix : public Matrix {
public:
    // --- Construction ---
    SquareMatrix(size_t n, const std::vector<std::vector<double>>& data);
    explicit SquareMatrix(size_t n);  // zero-initialised n x n matrix

    // --- Rule of Five (explicit delegation to base) ---
    SquareMatrix(const SquareMatrix& m);
    SquareMatrix(SquareMatrix&& m) noexcept;
    SquareMatrix& operator=(const SquareMatrix& m);
    SquareMatrix& operator=(SquareMatrix&& m) noexcept;
    ~SquareMatrix() override = default;  // base destructor is virtual; = default is fine here

    // --- Virtual override ---
    std::string name() const override;  // returns "SquareMatrix(n)"

    // --- SquareMatrix-specific interface ---
    size_t dimension() const { return _rows; }
    double trace() const;
};

SquareMatrix.cpp

#include "SquareMatrix.h"
#include <numeric>
#include <string>

SquareMatrix::SquareMatrix(size_t n,
                           const std::vector<std::vector<double>>& data)
    : Matrix(n, n, data) {}

SquareMatrix::SquareMatrix(size_t n)
    : Matrix(n, n) {}

SquareMatrix::SquareMatrix(const SquareMatrix& m)
    : Matrix(m) {}             // explicit base copy constructor call — required

SquareMatrix::SquareMatrix(SquareMatrix&& m) noexcept
    : Matrix(std::move(m)) {}  // cast to Matrix&&; base move constructor steals resources

SquareMatrix& SquareMatrix::operator=(const SquareMatrix& m) {
    Matrix::operator=(m);      // delegate to base; handles _rows, _cols, _data
    return *this;
}

SquareMatrix& SquareMatrix::operator=(SquareMatrix&& m) noexcept {
    Matrix::operator=(std::move(m));
    return *this;
}

std::string SquareMatrix::name() const {
    return "SquareMatrix(" + std::to_string(_rows) + ")";
}

double SquareMatrix::trace() const {
    double t = 0.0;
    for (size_t i = 0; i < _rows; ++i)
        t += _data[i][i];
    return t;
}

main.cpp — Demonstrating Virtual Dispatch

#include <iostream>
#include <memory>
#include "Matrix.h"
#include "SquareMatrix.h"

int main() {
    // --- 1. Virtual dispatch through a base pointer ---
    {
        std::vector<std::vector<double>> data = {{1,2,3},{4,5,6},{7,8,9}};

        std::unique_ptr<Matrix> p = std::make_unique<SquareMatrix>(3, data);

        // Virtual dispatch: even though p is Matrix*, the dynamic type is SquareMatrix.
        // name() resolves to SquareMatrix::name() at runtime via the vtable.
        std::cout << p->name() << "\n";  // prints: SquareMatrix(3)

        // Without 'virtual': p->name() would print "Matrix" — wrong.
    }

    // --- 2. Copying a SquareMatrix through the base interface ---
    {
        std::vector<std::vector<double>> d = {{1,0},{0,1}};
        SquareMatrix sq(2, d);

        // Copy construction: SquareMatrix(const SquareMatrix&) delegates to Matrix(const Matrix&)
        SquareMatrix sq2(sq);
        std::cout << sq2.name() << " trace=" << sq2.trace() << "\n";  // SquareMatrix(2) trace=2

        // Move construction
        SquareMatrix sq3(std::move(sq2));
        std::cout << sq3.name() << "\n";  // SquareMatrix(2)
    }

    // --- 3. Virtual destructor importance ---
    {
        // p has static type Matrix* but dynamic type SquareMatrix*.
        // When p goes out of scope (or is explicitly deleted), the compiler
        // looks up the destructor through the vtable:
        //   - vtable slot for ~Matrix points to ~SquareMatrix (the most-derived)
        //   - ~SquareMatrix() runs first, then ~Matrix()
        //   - All resources are correctly freed.
        //
        // WITHOUT virtual ~Matrix(): the vtable slot does not exist.
        // The compiler calls ~Matrix() directly (static dispatch on Matrix*).
        // ~SquareMatrix() is NEVER called. If SquareMatrix owned heap resources,
        // they would leak. This is undefined behaviour.
        std::unique_ptr<Matrix> p = std::make_unique<SquareMatrix>(4);
        // p's destructor calls ~SquareMatrix() correctly because ~Matrix() is virtual.
        std::cout << "Destructor: OK (run under ASan to confirm no leaks)\n";
    }

    // --- 4. dynamic_cast: RTTI check on dynamic type ---
    {
        std::vector<std::vector<double>> d = {{9,0},{0,9}};
        std::unique_ptr<Matrix> p = std::make_unique<SquareMatrix>(2, d);

        SquareMatrix* sq = dynamic_cast<SquareMatrix*>(p.get());
        if (sq) {
            std::cout << "dynamic_cast succeeded; trace=" << sq->trace() << "\n";
        }

        // dynamic_cast to an unrelated type returns nullptr (safe, not UB)
        // Only possible when the class has at least one virtual function (RTTI is attached)
    }

    return 0;
}

Compile and run (C++17):

g++ -std=c++17 -Wall -Wextra -Wpedantic -fsanitize=address,undefined \
    Matrix.cpp SquareMatrix.cpp main.cpp -o demo
./demo

Expected output:

SquareMatrix(3)
SquareMatrix(2) trace=2
SquareMatrix(2)
Destructor: OK (run under ASan to confirm no leaks)
dynamic_cast succeeded; trace=18

Validation

vptr Overhead

#include <iostream>
struct NoPoly  { double x; double y; };   // no virtual functions
struct Poly    { virtual ~Poly() = default; double x; double y; };

int main() {
    // On any 64-bit implementation: vptr is 8 bytes, inserted before data members.
    static_assert(sizeof(Poly) > sizeof(NoPoly),
                  "Polymorphic class must be larger due to vptr");
    std::cout << "sizeof(NoPoly)=" << sizeof(NoPoly)   // 16
              << "  sizeof(Poly)="  << sizeof(Poly)     // 24
              << "\n";
}

This confirms the hidden vptr cost. It is a one-time per-object cost, not per-call.

dynamic_cast RTTI Validation

dynamic_cast requires that the class hierarchy has at least one virtual function (which makes RTTI information available). If the cast succeeds (non-null result for pointer casts), the runtime confirms the dynamic type is correct:

Matrix* p = new SquareMatrix(3);
assert(dynamic_cast<SquareMatrix*>(p) != nullptr);   // succeeds: correct dynamic type
assert(dynamic_cast<SquareMatrix*>(p) == p);          // same address (single-inheritance)
delete p;  // virtual destructor ensures correct cleanup

ASan / Valgrind

Compile with -fsanitize=address (GCC/Clang). With the virtual destructor in place, ASan reports zero leaks. Without it (remove virtual from ~Matrix and re-test), ASan will report the SquareMatrix destructor was not called, and any heap memory owned by SquareMatrix will show as a leak. This is the easiest way to experimentally confirm the rule.


Limitations and Pitfalls

Object Slicing

SquareMatrix sq(3, data);
Matrix m = sq;   // SLICED: copies only the Matrix sub-object
std::cout << m.name();  // "Matrix" — virtual dispatch is gone

Assigning or copy-constructing a derived object into a base object discards all derived-class data and resets the vptr to Matrix's vtable. There is no error, no warning — the code compiles and runs with silently wrong semantics. Always use pointers or references for polymorphism. This is why PathSimulator stores a Model*, not a Model.

Calling Virtual Functions from Constructors or Destructors

During execution of Matrix::Matrix(...), the vtable is set to Matrix's vtable — SquareMatrix's vtable is not yet in place. Any virtual call inside the Matrix constructor calls Matrix::f(), not SquareMatrix::f(), even if SquareMatrix overrides it.

Matrix::Matrix(size_t r, size_t c) {
    // DANGEROUS: if name() is virtual and called here,
    // it calls Matrix::name(), not SquareMatrix::name()
    // std::cout << name() << "\n";  // always "Matrix", regardless of actual type
}

The same applies in reverse during destruction: when ~SquareMatrix() has returned and ~Matrix() is running, the object's vtable has been reset to Matrix's vtable. Never rely on virtual dispatch in constructors or destructors.

protected and private Inheritance

class SquareMatrix : protected Matrix {} and class SquareMatrix : private Matrix {} break the is-a relationship. A SquareMatrix cannot be implicitly converted to Matrix*. These forms represent "implemented-in-terms-of" rather than "is-a". They are rarely appropriate and should be avoided in standard quant model hierarchies.

Diamond Inheritance

class A { virtual void f(); };
class B : public A {};
class C : public A {};
class D : public B, public C {};  // D has two copies of A's sub-object

Without virtual base classes, D contains two A sub-objects; ambiguous method resolution and memory waste result. virtual inheritance solves the diamond but introduces additional complexity (offset calculations, virtual base pointer overhead). Avoid diamond hierarchies entirely in production quant code — prefer composition over multiple inheritance.

final Specifier

class SquareMatrix final : public Matrix {} prevents further derivation. std::string name() const override final; prevents further override. Use final where derivation would break invariants (e.g., a class with a hand-optimised SIMD kernel that must not be subclassed).


Interview Angle

Junior (L1): "What is object slicing and when does it occur?"

Expected answer: Object slicing occurs when a derived-class object is assigned or copy-constructed into a base-class object (not through a pointer or reference). Only the base sub-object is copied; derived-class members are discarded. The result is a genuine Matrix object with no virtual dispatch to SquareMatrix. It occurs silently — no compiler warning. Prevention: always pass polymorphic objects by pointer or reference, never by value.

Mid-level (L2): "Explain the vtable mechanism in concrete terms."

Expected answer: Every class with at least one virtual function has a compile-time vtable: a static array of function pointers, one slot per virtual method. Every object of such a class carries a hidden vptr (8 bytes on 64-bit), set by the constructor to point to the class's vtable. A virtual call p->f() compiles to: load vptr from *p, index the vtable at f's known offset, call through the resulting function pointer. Cost: one extra memory dereference per call. The vptr is set in constructors in top-down order (base constructor sets it to the base vtable; derived constructor then updates it to the derived vtable), which is why virtual calls in constructors do not dispatch to derived overrides.

Mid-level (L2): "Why must a polymorphic base class have a virtual destructor?"

Expected answer: When delete base_ptr is called and the pointed-to object's dynamic type is a derived class, the correct sequence is: call the derived destructor first, then the base destructor (in reverse construction order). Without a virtual destructor, the compiler performs static dispatch on the pointer's declared type (Matrix*), calling only ~Matrix(). The derived destructor is never invoked. Any resources owned by the derived class (heap memory, file descriptors, etc.) are leaked. This is undefined behaviour per the standard. The fix is virtual ~Matrix() = default; in the base, which makes the destructor virtual and causes the correct derived-then-base sequence.

Mid-level (L2): "What does override add compared to just re-declaring the same signature in the derived class?"

Expected answer: Without override, C++ silently accepts a method in the derived class that does not match any virtual in the base — it simply introduces a new, unrelated member function. Common causes: a different const-qualification, a slightly different parameter type, a name typo. With override, the compiler verifies that a matching virtual exists in a base class; if not, it is a hard compile error. override also documents intent — it signals to the reader that polymorphic behaviour is expected. Always use it: the cost is zero, the protection is significant.

Senior (L2+): "In a quant library, you have a PathSimulator that holds a Model*. How do you safely copy a PathSimulator?"

Expected answer: You cannot simply copy the Model* — both objects would share the same underlying model, and the second destructor would double-delete it. You need the clone pattern (Module 4): Model declares virtual Model* clone() const = 0;; each concrete model implements it as return new BlackScholesModel(*this);. The PathSimulator copy constructor calls _model->clone() to obtain a heap-allocated, type-correct copy. This is the standard solution for polymorphic ownership in pre-C++11 code and is still the idiomatic approach when std::unique_ptr semantics are insufficient (e.g., when the abstract base must be copied through an interface).