TL;DR – C++ templates enable compile-time DSLs with zero runtime cost. This guide shows expression templates, type-level computations, and compile-time validation for financial calculations.
Templates provide:
Build expression trees at compile time:
1#include <iostream>
2#include <cmath>
3
4// Base expression
5template<typename E>
6struct Expr {
7 double operator[](size_t i) const {
8 return static_cast<const E&>(*this)[i];
9 }
10
11 size_t size() const {
12 return static_cast<const E&>(*this).size();
13 }
14};
15
16// Vector wrapper
17class Vector : public Expr<Vector> {
18 double* data_;
19 size_t size_;
20
21public:
22 Vector(size_t n) : data_(new double[n]), size_(n) {}
23 ~Vector() { delete[] data_; }
24
25 double operator[](size_t i) const { return data_[i]; }
26 double& operator[](size_t i) { return data_[i]; }
27 size_t size() const { return size_; }
28
29 // Assignment from expression
30 template<typename E>
31 Vector& operator=(const Expr<E>& expr) {
32 for (size_t i = 0; i < size_; ++i) {
33 data_[i] = expr[i];
34 }
35 return *this;
36 }
37};
38
39// Binary operation
40template<typename Op, typename L, typename R>
41struct BinOp : public Expr<BinOp<Op, L, R>> {
42 const L& lhs;
43 const R& rhs;
44
45 BinOp(const L& l, const R& r) : lhs(l), rhs(r) {}
46
47 double operator[](size_t i) const {
48 return Op::apply(lhs[i], rhs[i]);
49 }
50
51 size_t size() const { return lhs.size(); }
52};
53
54// Operations
55struct Add {
56 static double apply(double a, double b) { return a + b; }
57};
58
59struct Mul {
60 static double apply(double a, double b) { return a * b; }
61};
62
63// Operator overloads
64template<typename L, typename R>
65BinOp<Add, L, R> operator+(const Expr<L>& lhs, const Expr<R>& rhs) {
66 return BinOp<Add, L, R>(
67 static_cast<const L&>(lhs),
68 static_cast<const R&>(rhs)
69 );
70}
71
72template<typename L, typename R>
73BinOp<Mul, L, R> operator*(const Expr<L>& lhs, const Expr<R>& rhs) {
74 return BinOp<Mul, L, R>(
75 static_cast<const L&>(lhs),
76 static_cast<const R&>(rhs)
77 );
78}
79
80// Usage: portfolio value calculation
81Vector prices(1000);
82Vector quantities(1000);
83Vector weights(1000);
84
85// Lazy evaluation - no temporaries
86Vector portfolio_value(1000);
87portfolio_value = prices * quantities * weights;
88Benefit: Zero temporary allocations, optimal code generation.
Type-safe dimensional analysis:
1#include <ratio>
2
3template<int M, int L, int T>
4struct Unit {
5 double value;
6
7 constexpr explicit Unit(double v) : value(v) {}
8
9 constexpr double get() const { return value; }
10};
11
12// Dimension aliases
13template<typename T> using Scalar = Unit<0, 0, 0>;
14template<typename T> using Price = Unit<1, 0, 0>; // Money
15template<typename T> using Quantity = Unit<0, 1, 0>; // Count
16template<typename T> using Time = Unit<0, 0, 1>; // Time
17
18// Multiplication: dimensions add
19template<int M1, int L1, int T1, int M2, int L2, int T2>
20constexpr Unit<M1+M2, L1+L2, T1+T2> operator*(
21 Unit<M1, L1, T1> lhs,
22 Unit<M2, L2, T2> rhs
23) {
24 return Unit<M1+M2, L1+L2, T1+T2>(lhs.value * rhs.value);
25}
26
27// Division: dimensions subtract
28template<int M1, int L1, int T1, int M2, int L2, int T2>
29constexpr Unit<M1-M2, L1-L2, T1-T2> operator/(
30 Unit<M1, L1, T1> lhs,
31 Unit<M2, L2, T2> rhs
32) {
33 return Unit<M1-M2, L1-L2, T1-T2>(lhs.value / rhs.value);
34}
35
36// Addition: same dimensions only
37template<int M, int L, int T>
38constexpr Unit<M, L, T> operator+(Unit<M, L, T> lhs, Unit<M, L, T> rhs) {
39 return Unit<M, L, T>(lhs.value + rhs.value);
40}
41
42// Usage: compile-time type checking
43constexpr Price<double> price{100.50};
44constexpr Quantity<double> qty{1000};
45constexpr auto notional = price * qty; // Unit<1, 1, 0> - valid
46
47// This won't compile: type mismatch
48// auto invalid = price + qty; // Error: different dimensions
49Safety: Dimensional errors caught at compile time.
Curiously Recurring Template Pattern:
1template<typename Derived>
2class Instrument {
3public:
4 double npv() const {
5 return static_cast<const Derived*>(this)->npv_impl();
6 }
7
8 double delta() const {
9 return static_cast<const Derived*>(this)->delta_impl();
10 }
11};
12
13class EuropeanOption : public Instrument<EuropeanOption> {
14 double spot_, strike_, rate_, vol_, time_;
15
16public:
17 EuropeanOption(double s, double k, double r, double v, double t)
18 : spot_(s), strike_(k), rate_(r), vol_(v), time_(t) {}
19
20 double npv_impl() const {
21 // Black-Scholes formula
22 double d1 = (std::log(spot_ / strike_) +
23 (rate_ + 0.5 * vol_ * vol_) * time_) /
24 (vol_ * std::sqrt(time_));
25 double d2 = d1 - vol_ * std::sqrt(time_);
26
27 return spot_ * norm_cdf(d1) -
28 strike_ * std::exp(-rate_ * time_) * norm_cdf(d2);
29 }
30
31 double delta_impl() const {
32 double d1 = (std::log(spot_ / strike_) +
33 (rate_ + 0.5 * vol_ * vol_) * time_) /
34 (vol_ * std::sqrt(time_));
35 return norm_cdf(d1);
36 }
37
38private:
39 static double norm_cdf(double x) {
40 return 0.5 * (1.0 + std::erf(x / std::sqrt(2.0)));
41 }
42};
43
44// Generic pricing function
45template<typename T>
46double price_instrument(const Instrument<T>& inst) {
47 return inst.npv(); // Statically dispatched, no vtable
48}
49
50// Usage
51EuropeanOption option(100, 100, 0.05, 0.2, 1.0);
52double price = price_instrument(option); // Zero overhead
53Performance: No virtual function overhead, fully inlined.
Type-safe variadic functions:
1#include <tuple>
2#include <type_traits>
3
4// Fold expression for sum
5template<typename... Args>
6constexpr auto sum(Args... args) {
7 return (args + ...);
8}
9
10// Generic Monte Carlo simulation
11template<typename RNG, typename Payoff, typename... Params>
12double monte_carlo(size_t n_sims, RNG& rng, Payoff payoff, Params... params) {
13 double sum = 0.0;
14
15 for (size_t i = 0; i < n_sims; ++i) {
16 sum += payoff(rng, params...);
17 }
18
19 return sum / n_sims;
20}
21
22// Example payoff function
23struct CallPayoff {
24 template<typename RNG>
25 double operator()(RNG& rng, double spot, double strike, double vol, double time) const {
26 double z = rng.normal();
27 double st = spot * std::exp(-0.5 * vol * vol * time + vol * std::sqrt(time) * z);
28 return std::max(0.0, st - strike);
29 }
30};
31
32// Usage
33std::mt19937 rng;
34CallPayoff payoff;
35double price = monte_carlo(1'000'000, rng, payoff, 100.0, 100.0, 0.2, 1.0);
36Flexibility: Type-safe, arbitrary parameters.
Compute prices at compile time:
1constexpr double constexpr_sqrt(double x, double guess = 1.0, int iter = 0) {
2 return iter == 10 ? guess :
3 constexpr_sqrt(x, (guess + x / guess) / 2.0, iter + 1);
4}
5
6constexpr double constexpr_exp(double x) {
7 double result = 1.0;
8 double term = 1.0;
9 for (int i = 1; i < 20; ++i) {
10 term *= x / i;
11 result += term;
12 }
13 return result;
14}
15
16constexpr double constexpr_log(double x) {
17 // Newton's method
18 double y = x - 1.0;
19 double result = 0.0;
20 double term = y;
21 for (int i = 1; i < 20; ++i) {
22 result += term / i;
23 term *= -y;
24 }
25 return result;
26}
27
28// Compile-time Black-Scholes (simplified)
29constexpr double black_scholes_constexpr(
30 double spot, double strike, double rate, double vol, double time
31) {
32 double d1 = (constexpr_log(spot / strike) +
33 (rate + 0.5 * vol * vol) * time) /
34 (vol * constexpr_sqrt(time));
35
36 // Simplified - full implementation needs erf
37 return spot * 0.5 - strike * constexpr_exp(-rate * time) * 0.5;
38}
39
40// Compile-time constant
41constexpr double atm_call_price = black_scholes_constexpr(100, 100, 0.05, 0.2, 1.0);
42static_assert(atm_call_price > 0.0, "Price must be positive");
43Benefit: Validation at compile time, zero runtime cost.
Pre-C++20 concept emulation:
1#include <type_traits>
2
3// Check if type has npv() method
4template<typename T, typename = void>
5struct has_npv : std::false_type {};
6
7template<typename T>
8struct has_npv<T, std::void_t<decltype(std::declval<T>().npv())>>
9 : std::true_type {};
10
11// Check if type is priceable
12template<typename T>
13constexpr bool is_priceable_v = has_npv<T>::value;
14
15// Generic pricing with SFINAE
16template<typename T>
17std::enable_if_t<is_priceable_v<T>, double>
18price(const T& instrument) {
19 return instrument.npv();
20}
21
22// Won't compile for non-priceable types
23// double p = price(42); // Error: int is not priceable
24Safety: Type constraints enforced at compile time.
Optimize for specific types:
1// Generic matrix multiplication
2template<typename T>
3void matmul(const T* A, const T* B, T* C, size_t n) {
4 for (size_t i = 0; i < n; ++i) {
5 for (size_t j = 0; j < n; ++j) {
6 C[i * n + j] = 0;
7 for (size_t k = 0; k < n; ++k) {
8 C[i * n + j] += A[i * n + k] * B[k * n + j];
9 }
10 }
11 }
12}
13
14// Specialized for double - use BLAS
15template<>
16void matmul<double>(const double* A, const double* B, double* C, size_t n) {
17 // Call optimized BLAS routine
18 cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans,
19 n, n, n, 1.0, A, n, B, n, 0.0, C, n);
20}
21Performance: Optimal code path per type.
Embedded domain-specific language:
1// Trade DSL
2struct Trade {
3 std::string symbol;
4 double quantity;
5 double price;
6
7 Trade& buy(double qty) {
8 quantity = qty;
9 return *this;
10 }
11
12 Trade& at(double p) {
13 price = p;
14 return *this;
15 }
16};
17
18Trade operator""_shares(const char* sym, size_t) {
19 return Trade{sym, 0, 0};
20}
21
22// Usage: natural syntax
23auto trade = "AAPL"_shares.buy(100).at(150.50);
24Readability: Domain-specific syntax for clarity.
Compile-time matrix operations:
1template<size_t N>
2class CovarianceMatrix {
3 std::array<std::array<double, N>, N> data_;
4
5public:
6 constexpr CovarianceMatrix() : data_{} {}
7
8 constexpr double& operator()(size_t i, size_t j) {
9 return data_[i][j];
10 }
11
12 constexpr double operator()(size_t i, size_t j) const {
13 return data_[i][j];
14 }
15
16 // Compile-time Cholesky decomposition
17 constexpr CovarianceMatrix<N> cholesky() const {
18 CovarianceMatrix<N> L;
19
20 for (size_t i = 0; i < N; ++i) {
21 for (size_t j = 0; j <= i; ++j) {
22 double sum = 0.0;
23
24 for (size_t k = 0; k < j; ++k) {
25 sum += L(i, k) * L(j, k);
26 }
27
28 if (i == j) {
29 L(i, j) = constexpr_sqrt((*this)(i, i) - sum);
30 } else {
31 L(i, j) = ((*this)(i, j) - sum) / L(j, j);
32 }
33 }
34 }
35
36 return L;
37 }
38};
39
40// Compile-time computation
41constexpr CovarianceMatrix<3> cov = []() {
42 CovarianceMatrix<3> m;
43 m(0, 0) = 1.0; m(0, 1) = 0.5; m(0, 2) = 0.3;
44 m(1, 0) = 0.5; m(1, 1) = 1.0; m(1, 2) = 0.4;
45 m(2, 0) = 0.3; m(2, 1) = 0.4; m(2, 2) = 1.0;
46 return m;
47}();
48
49constexpr auto L = cov.cholesky(); // Computed at compile time
50Benefit: Matrix decomposition done at build time.
C++ template metaprogramming enables zero-overhead domain-specific languages for financial calculations. Use expression templates for lazy evaluation, constexpr for compile-time computation, and CRTP for static polymorphism to build high-performance, type-safe pricing libraries.
Technical Writer
NordVarg Team is a software engineer at NordVarg specializing in high-performance financial systems and type-safe programming.
Get weekly insights on building high-performance financial systems, latest industry trends, and expert tips delivered straight to your inbox.