Quant developer HFT course · Module 3: Containers and memory
Templates, CRTP and compile-time polymorphism
The question
You want a feed handler that works with several strategies without virtual function calls on every message. How would you design it?
What a template does
A template is a pattern from which the compiler generates code. Each instantiation, Handler<Momentum> or Handler<Arbitrage>, is compiled separately with the type known exactly. Calls inside it go straight to the right functions, which can be inlined; constants known at compile time are folded; sizes are fixed.
template <class Strategy>
class Handler {
Strategy& s_;
public:
explicit Handler(Strategy& s) : s_(s) {}
void on_message(const Quote& q) {
if (q.qty == 0) s_.on_delete(q);
else s_.on_update(q); // direct call, inlinable
}
};
That is the basic answer to the question: make the hot component a template on the strategy type. One binary can contain several instantiations; one process usually runs one.
CRTP
Sometimes you want a base class with shared logic that calls back into the derived class, as you would with virtual functions, but without dispatch at run time. The curiously recurring template pattern does this: the derived class passes itself as the base's template argument.
template <class Derived>
class StrategyBase {
public:
void on_quote(const Quote& q) {
if (!risk_ok(q)) return; // shared logic in the base
static_cast<Derived*>(this)->handle(q); // calls the derived class directly
}
private:
bool risk_ok(const Quote& q) const { return q.qty < max_qty_; }
std::int32_t max_qty_ = 1000;
};
class Momentum : public StrategyBase<Momentum> {
public:
void handle(const Quote& q); // no virtual, no override
};
The static_cast is resolved at compile time because the base knows exactly what Derived is. There is no vtable, no vptr in the object, and handle can be inlined into on_quote. What you give up is a common base type: StrategyBase<Momentum> and StrategyBase<Arbitrage> are unrelated classes, so you cannot keep both in one container of base pointers. That is the point; heterogeneous collections are what virtual functions are for.
C++23's explicit object parameter ("deducing this") gives the same effect more simply: void on_quote(this auto& self, const Quote& q) lets the base call self.handle(q) with the derived type deduced.
Concepts
Templates accept any type, and an error from a missing member used to produce pages of output. C++20 concepts state the requirements up front:
template <class S>
concept QuoteStrategy = requires(S s, const Quote& q) {
{ s.on_update(q) } -> std::same_as<void>;
{ s.on_delete(q) } -> std::same_as<void>;
};
template <QuoteStrategy Strategy>
class Handler { /* ... */ };
A type that does not satisfy the concept is rejected at the point of use with a message naming the requirement. Concepts cost nothing at run time.
Compile-time computation
Templates and constexpr move work from run time to compile time. Table sizes, bit masks, message layouts and dispatch tables can be computed once by the compiler:
constexpr std::array<std::int64_t, 10> POW10 = [] {
std::array<std::int64_t, 10> a{}; a[0] = 1;
for (int i = 1; i < 10; ++i) a[i] = a[i - 1] * 10;
return a;
}(); // computed during compilation
if constexpr removes branches that depend on a template parameter entirely from the instantiation that does not need them, so one template can serve a feed with and without a feature at no run-time cost.
The costs
- Code size. Each instantiation is a separate copy of the code. Many instantiations of a large template can bloat the binary and, more importantly for latency, the instruction cache.
- Compile time. Heavy templates in headers slow every build that includes them.
- Errors and readability. Concepts help; deep template machinery still makes code harder to review.
A balanced design keeps the hot path generic over a small number of types and leaves configuration, start-up and administration as ordinary code with virtual interfaces where convenient.
Check yourself
Question 1 of 3
What does CRTP replace?
The base casts this to the derived type known from its template argument, so calls are direct and inlinable instead of going through a vtable.
Question 2 of 3
What can you not do with CRTP that virtual functions allow?
Each Base<Derived> is a distinct type, so there is no common base to point through. That is the trade for removing run-time dispatch.
Question 3 of 3
What is a real cost of instantiating a large template for many types?
Templates are compiled per type. The run-time calls are direct; the cost is code size, which matters when hot code no longer fits in the instruction cache.
In the interview
Say your own answer out loud first, then open the one we would give.
How do you get polymorphism without virtual functions?
Make the code that calls the polymorphic behaviour a template on the concrete type, so calls are resolved and inlined at compile time. For shared base-class logic that calls back into the derived class, CRTP or C++23's deducing this. For a closed set of types chosen at run time, std::variant with std::visit.
What is CRTP?
A class derives from a base template instantiated with itself, so the base can static_cast this to the derived type and call it directly. It gives the structure of virtual functions with no vtable and full inlining, at the cost of having no common runtime base type.
What are the downsides of heavy template use?
Larger binaries and instruction-cache pressure from many instantiations, slower compile times, and code that is harder to read and debug. I keep templates to the hot path and to a small number of instantiations, and use concepts to make the requirements explicit.