Skip to content
Work Practice Products Coding interviews Machine learning coding Quant developer HFT course Blog Method Why me About Enquire Book a call

Quant developer HFT course · Module 3: Containers and memory

Templates, CRTP and compile-time polymorphism

Lesson · 11 min

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.

resolved at compile timeHandler<Momentum>on_quote(q): static_castMomentumhandle(q), inlined
CRTP: the base class knows the derived type as a template argument, so the call is an ordinary, inlinable function call. No vtable is involved.
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?

Question 2 of 3

What can you not do with CRTP that virtual functions allow?

Question 3 of 3

What is a real cost of instantiating a large template for many types?

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.