C++Abstract ClassesPure VirtualClone PatternPolymorphic Ownership

Abstract Classes and the Clone Pattern

Module 4 of 822 min readLevel: Medium

Setup

The Polymorphic Ownership Problem

The previous module showed how virtual dispatch allows a Model* to point to either a BlackScholesModel or a DupireLocalVolatilityModel at runtime. But as soon as you introduce polymorphism, you face a question that the language does not answer for you:

How do you copy a polymorphic object?

Consider PathSimulator, which owns a Model*. When you copy a PathSimulator, you want a deep copy of the Model it points to — not a pointer alias (which would cause a double-free) and not a copy of the base class slice (which would lose the derived-class state). The copy constructor cannot call new Model(*_model) because Model is abstract and because even if it were concrete, that would slice the object.

The solution is the clone pattern: each class in the hierarchy implements a virtual clone() method that returns a heap-allocated copy of itself, preserving the fully derived type. This is sometimes called the virtual copy constructor idiom.

This module builds the pattern from scratch using the Distribution hierarchy: an abstract base representing a probability distribution, with concrete NormalDistribution and LogNormalDistribution derived classes. The same pattern appears verbatim in the production MC pricer (Model::clone(), PathSimulator::clone(), Payoff::clone()).

Conventions used throughout:

  • All class definitions use the interface/implementation split (.h / .cpp).
  • The base class destructor is virtual. Without it, deleting a derived object through a base pointer is undefined behaviour.
  • Raw owning pointers are used here to expose the mechanics. In production code, wrap in std::unique_ptr.

Theory

1. Abstract Classes and Pure Virtual Methods

A class is abstract if it declares at least one pure virtual method:

virtual double pdf(double x) const = 0;  // pure virtual

The = 0 syntax tells the compiler: this class provides no implementation for pdf; any concrete derived class must provide one. An abstract class cannot be instantiated directly — attempting to write Distribution d("normal") is a compile error.

Pure virtual methods define an interface contract. Every class that inherits from Distribution and wishes to be instantiable must override pdf. If a derived class fails to override a pure virtual method, it too becomes abstract.

The override keyword in derived classes is mandatory on this platform:

double pdf(double x) const override;  // confirms we are overriding a base virtual

The compiler catches a silent bug: if you misspell the method name or change the signature, override produces a compile error instead of silently creating a new unrelated method.

2. The Object Slicing Problem

Slicing occurs when a derived object is copied into a base object, discarding the derived parts:

NormalDistribution nd("N", 0.0, 1.0);
Distribution d = nd;   // SLICED: d is a Distribution, _mean and _variance are gone

Slicing is silent and catastrophic in a numerical context. If Distribution had a data member _name and NormalDistribution adds _mean and _variance, the sliced copy drops both parameters. Any subsequent pdf call on the copy would compute rubbish — or, if pdf remained pure virtual, the copy would fail to compile anyway.

In a pricing context, slicing turns a calibrated stochastic vol model into an empty shell. The rule:

Polymorphic base classes should be copied only through the clone pattern — never by value.

Enforce this by deleting the copy operations in the base class or by making the base class move-only.

3. The Clone Pattern

The solution is to delegate copying to a virtual method:

class Distribution {
public:
    virtual Distribution* clone() const = 0;
    // ...
};

class NormalDistribution : public Distribution {
public:
    NormalDistribution* clone() const override {
        return new NormalDistribution(*this);  // calls NormalDistribution copy ctor
    }
};

Three properties make this work:

  1. Covariant return type: NormalDistribution* is a valid override of Distribution* because NormalDistribution is derived from Distribution. The caller holding a Distribution* gets a Distribution*; the caller holding a NormalDistribution* gets a NormalDistribution*. No cast required in either case.

  2. Full derived type preserved: new NormalDistribution(*this) calls the NormalDistribution copy constructor, which copies all members including _mean and _variance. No slicing.

  3. Heap allocation: clone() always returns a heap pointer. The caller owns the returned object and is responsible for its deallocation (or wraps it in std::unique_ptr<Distribution>).

4. The Rule of Five for Polymorphic Classes

When a class manages a resource (here: owns a raw pointer to a heap-allocated Distribution), you must define or suppress all five special member functions consistently.

MemberPurposeAction for owning container
DestructorRelease owned resourcedelete _distribution;
Copy constructorDeep-copy owned resource_distribution = other._distribution->clone();
Copy assignmentDeep-copy, release olddelete _distribution; _distribution = other._distribution->clone();
Move constructorTransfer ownership_distribution = other._distribution; other._distribution = nullptr;
Move assignmentTransfer, release olddelete _distribution; _distribution = other._distribution; other._distribution = nullptr;

If you define a destructor that deletes the pointer but omit the copy constructor, the compiler generates a memberwise copy — both the original and the copy share the same raw pointer. When the first destructor fires, the pointer is freed. When the second destructor fires, the pointer is freed again: double-free, undefined behaviour.

The copy constructor of PathSimulator uses clone precisely to avoid this:

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.