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:
-
Covariant return type:
NormalDistribution*is a valid override ofDistribution*becauseNormalDistributionis derived fromDistribution. The caller holding aDistribution*gets aDistribution*; the caller holding aNormalDistribution*gets aNormalDistribution*. No cast required in either case. -
Full derived type preserved:
new NormalDistribution(*this)calls theNormalDistributioncopy constructor, which copies all members including_meanand_variance. No slicing. -
Heap allocation:
clone()always returns a heap pointer. The caller owns the returned object and is responsible for its deallocation (or wraps it instd::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.
| Member | Purpose | Action for owning container |
|---|---|---|
| Destructor | Release owned resource | delete _distribution; |
| Copy constructor | Deep-copy owned resource | _distribution = other._distribution->clone(); |
| Copy assignment | Deep-copy, release old | delete _distribution; _distribution = other._distribution->clone(); |
| Move constructor | Transfer ownership | _distribution = other._distribution; other._distribution = nullptr; |
| Move assignment | Transfer, release old | delete _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: