Setup
Classes as the Unit of Abstraction in Quant Code
In production quant finance, every reusable computation is a class. A term-structure model is a class. A Monte Carlo path generator is a class. A payoff function is a class. A matrix is a class. This is not organizational ceremony — it is the mechanism by which C++ enforces encapsulation, enables RAII (Resource Acquisition Is Initialisation), and provides the compiler with enough information to eliminate redundant copies.
The central concern of this module is object lifetime. When you pass a matrix to a function, does it copy the data? When you return a matrix from a function, does a copy occur? When a path generator holds a std::vector<double> of 100,000 simulated spot prices and it goes out of scope, is the memory freed? The answers to all of these questions are determined by the special member functions: the constructor, copy constructor, copy assignment operator, move constructor, move assignment operator, and destructor.
Understanding these six functions is a prerequisite for writing C++ that is both correct and efficient. The Rule of Five states: if you define (or delete) any one of {destructor, copy constructor, copy assignment, move constructor, move assignment}, you almost certainly need to define all five explicitly. The compiler's defaults are correct only when your class has no resources to manage. Once your class owns heap memory — directly via a raw pointer or indirectly via its own data members — the defaults will produce incorrect or dangerous behaviour.
The running example throughout this module is a Matrix class, as taught in the Gustave Eiffel M2 quantitative finance C++ course.
Theory
1. Class Anatomy: Headers, Access Control, and const Methods
C++ classes are declared in a header file (.h or .hpp) and defined in a source file (.cpp). The header is what consumers of the class #include; it must contain everything needed to use the class without seeing how it is implemented.
// What belongs in the header (.h):
// - The class declaration (member variables, function signatures)
// - Inline function definitions (small functions that benefit from inlining)
// - Template definitions (must be visible at instantiation point)
//
// What belongs in the source (.cpp):
// - Non-trivial function definitions
// - Definitions of static data members
Access specifiers control which code can access which members:
public: accessible from any code that has the object.protected: accessible from the class and its derived classes.private: accessible only from within the class itself.
For a Matrix class, the data (_nrows, _ncols, _data) is private or protected. The interface (nrows(), ncols(), row()) is public. This separation means clients cannot accidentally corrupt the matrix's internal state.
const member functions declare that the function will not modify *this:
size_t nrows() const { return _nrows; } // const: calling on a const Matrix& is valid
A const method can be called on both const and non-const objects. A non-const method can only be called on non-const objects. As a general rule: any accessor (getter) should be const.
static members belong to the class, not to any instance:
static constexpr size_t MAX_DIM = 10000; // shared across all Matrix instances
2. Constructors and the Initializer List
The constructor is called when an object is created. Its job is to put the object into a valid state.
Matrix(size_t nrows, size_t ncols, const std::vector<Vector>& data);
The initializer list (the colon-separated list between the parameter list and the function body) initializes member variables before the body executes:
Matrix::Matrix(size_t nrows, size_t ncols, const std::vector<Vector>& data)
: _nrows(nrows),
_ncols(ncols),
_data(data) // invokes std::vector's copy constructor
{
// Body executes after all members are already initialized
// Validation can go here:
// if (data.size() != nrows || ...) throw std::invalid_argument(...)
}
Why prefer the initializer list over assignment in the body?
Without the initializer list, the compiler first default-initializes each member (calling its default constructor), then runs the body, which assigns to the already-initialized member. That is one unnecessary construction plus one assignment. For std::vector, the default constructor allocates nothing; the assignment then copies. With the initializer list, the vector is copy-constructed directly from data — one operation instead of two.
For const members and reference members, the initializer list is mandatory — they cannot be assigned to after construction.
Members are initialized in declaration order, not in the order they appear in the initializer list. Always write the initializer list in declaration order to avoid subtle bugs where one member is initialized using another that hasn't been initialized yet.
3. Copy Constructor
The copy constructor initializes a new object as a copy of an existing one:
Matrix(const Matrix& other);
It is called when:
- A variable is initialized from another variable:
Matrix B = A; - An object is passed by value to a function:
void f(Matrix m)—mis copy-constructed from the argument. - An object is returned by value (when NRVO/RVO cannot be applied).
For Matrix, the copy constructor copies all three members:
Matrix::Matrix(const Matrix& other)
: _nrows(other._nrows),
_ncols(other._ncols),
_data(other._data) // std::vector's copy constructor: deep copies all elements
{
}
Deep copy vs. shallow copy:
std::vector<double> has its own copy constructor that allocates new memory and copies all elements. So _data(other._data) is a deep copy — the new matrix has its own independent data buffer.
If _data were instead a raw pointer (double* _data), the compiler-generated copy constructor would copy the pointer value — both matrices would point to the same memory. Modifying one would silently modify the other. Deleting one would leave the other with a dangling pointer. This is the shallow copy problem, and it is why any class holding raw heap pointers must provide an explicit copy constructor that deep-copies the data.
4. Copy Assignment Operator
The copy assignment operator replaces the state of an already-existing object:
Matrix& operator=(const Matrix& other);
It is called when you write A = B where A has already been constructed.
Two requirements that are absent from the copy constructor:
- Self-assignment guard:
A = Amust be safe. Without the guard, if your class frees its own resources before readingother's, andotheris*this, you read freed memory. - Free old resources:
Aalready holds data. If it owns heap memory, that memory must be freed before the new data is installed (with raw pointers).
Matrix& Matrix::operator=(const Matrix& other) {
if (this != &other) { // self-assignment guard
_nrows = other._nrows;
_ncols = other._ncols;
_data = other._data; // std::vector's operator= handles deallocation and copy
}
return *this; // enables chaining: A = B = C
}
Why return Matrix& (rather than void)? To support chained assignment: A = B = C is parsed as A = (B = C). The inner B = C must return a reference to B for the outer assignment to work.
5. Move Constructor
The move constructor transfers ownership of resources from a temporary (or explicitly moved) object rather than copying them:
Matrix(Matrix&& other) noexcept;
It is called when:
- An object is initialized from a temporary:
Matrix C = make_matrix(); - An object is initialized from
std::move(other):Matrix C = std::move(B);
After the move, other is in a valid but unspecified state — it is safe to destroy, but its contents are not specified. For std::vector, moving sets the source vector to empty (size() == 0).
Matrix::Matrix(Matrix&& other) noexcept
: _nrows(std::move(other._nrows)), // for POD types (size_t), std::move is a cast — copies the value
_ncols(std::move(other._ncols)),
_data(std::move(other._data)) // std::vector move: O(1) pointer swap; other._data becomes empty
{
}
Performance: moving a Matrix with 1000×1000 elements transfers 3 values (2 size_ts and one internal pointer inside std::vector) — O(1). Copying it copies all 10⁶ doubles — O(n²). This is the performance motivation for move semantics.
noexcept is critical. The C++ standard library — std::vector in particular — uses move operations during reallocation only if the move constructor is noexcept. If it is not, the standard says "fall back to copy" (to maintain exception safety). Without noexcept, a std::vector<Matrix> would copy every element on every reallocation, silently destroying the O(1) benefit of move semantics.
6. Move Assignment Operator
The move assignment operator transfers resources into an already-existing object:
Matrix& operator=(Matrix&& other) noexcept;
Matrix& Matrix::operator=(Matrix&& other) noexcept {
if (this != &other) { // self-assignment guard (unusual for moves, but safe)
_nrows = std::move(other._nrows);
_ncols = std::move(other._ncols);
_data = std::move(other._data);
}
return *this;
}
std::vector's move assignment frees the current buffer, then transfers the source's internal pointer in O(1). No element-by-element copy occurs.
7. Destructor
The destructor is called when an object's lifetime ends (scope exit, delete, container destruction). Its job is to release any resources the object owns.
virtual ~Matrix() = default;
For Matrix (which owns only std::vector members), = default is correct — std::vector's destructor frees the element storage. No explicit body is needed.
For a class with raw pointer members:
~RawMatrix() {
delete[] _data; // must free what the constructor allocated with new[]
}
virtual destructor: any class intended to be used as a base class in a polymorphic hierarchy must have a virtual destructor. Without it, delete base_ptr where base_ptr holds a Derived* calls only the base destructor — the derived class's resources are leaked. This is covered in the inheritance module; Matrix is marked virtual ~Matrix() here for forward compatibility.