← Back to list

Compile-Time Limit Order Book in C++ — Price Levels and Compile-Time Queues (Part 3)

This article is Part 3 in a five-part series where we build a compile-time limit order book using modern C++ template metaprogramming.

Rishabh Garg · 2026-02-15 23:31 · 1 claps · 5.5 min read
#order-book #template-metaprogramming #hft #cpp #interview
Open on Medium ↗
Wiki topics: 💻 · Programming

Compile-Time Limit Order Book in C++ — Price Levels and Compile-Time Queues (Part 3)

This article is Part 3 in a five-part series where we build a compile-time limit order book using modern C++ template metaprogramming.

If you haven’t read Parts 1 and 2, I strongly recommend doing so before continuing. They introduce the motivation, design constraints, and the Order template that we will build upon here.

Part 1 — Compile-Time Limit Order Book in C++ — A Template Metaprogramming Deep Dive Part 2 — Order Representation at Compile Time Part 3 — Price Levels and Compile-Time Queues (This article) Part 4 — Merge Sort and List of Price Levels Part 5 — Bringing It All Together The full source code is published on github — RishabhGarg108/compile_time_orderbook

What We Will Build in This Article

In this article, we introduce the concept of a price level and implement it entirely at compile time. By the end of this article, we will have:

  • A generic compile-time FIFO queue
  • A price level abstraction consisting of:
  • A price
  • A time-priority-preserving queue of orders
  • Compile-time operations to:
  • Add an order to a level
  • Remove an order from a level by orderId

Along the way, we will cover two important template metaprogramming concepts:

  • Expressing if / else logic using std::conditional_t
  • Understanding the difference between a template and a type, and how the global scope resolution operator (::) helps disambiguate them

🧠 Mental Model: How Recursive TMP Actually Executes

Every recursive template instantiation is a compile-time function call.

  • Template parameters are the inputs
  • The type alias is the return value
  • Partial specializations act like if / else branches

The compiler repeatedly:

  1. Matches the most specialized template
  2. Instantiates it
  3. Substitutes parameters
  4. Repeats until a base case is reached

If two base cases are equally valid, compilation fails — even if the algorithm itself is correct.

A Generic Compile-Time Queue

At each price level, orders must be stored according to time priority: orders that arrive earlier must be matched before later ones.

Conceptually, this is just a FIFO queue.

Why a Generic Queue?

At first glance, it might seem odd to build a queue that can store arbitrary types instead of only Orders.

In runtime programming, all orders would be instances of the same type. In template metaprogramming, however, each order is a distinct type:

Order<1, BUY, 100, 5>
Order<2, SELL, 101, 5>

These are not objects — they are different types. Therefore, our queue must be able to hold heterogeneous types.

Queue Interface

  • Queue: It is represented as a variadic type list. Elements at the front appear earlier in the parameter pack.
  • QueuePush: It appends a new element to the end of the queue, preserving FIFO order.
  • QueuePop: It removes the front element of the queue.
  • QueueMerge: It will be useful when implementing order removal.
/*////////////////////////////////////////////////
Queue: Generic queue that can hold heterogeneous
data types. It provides ability to push at the end
of queue and pop from the front.
////////////////////////////////////////////////*/
template <typename... T>
struct Queue
{
    static constexpr int size = sizeof...(T);
};

/*////////////////////////////////////////////////
QueuePush: Pushes an element at the end of the
queue.
////////////////////////////////////////////////*/
template <typename Q, typename T>
struct QueuePush;

template <typename T, typename... QElem>
struct QueuePush<Queue<QElem...>, T>
{
    using type = Queue<QElem..., T>;
};

template <typename Q, typename T>
using QueuePush_t = typename  QueuePush<Q, T>::type;

/*////////////////////////////////////////////////
QueuePop: Pops an element front the front of the
queue.
////////////////////////////////////////////////*/
template <typename Q>
struct QueuePop;

template <typename T0, typename... T1toN>
struct QueuePop<Queue<T0, T1toN...>>
{
    using type = Queue<T1toN...>;
};

template <typename Q>
using QueuePop_t = typename QueuePop<Q>::type;

/*////////////////////////////////////////////////
QueueMerge: Merges two queues by pushing elements
from the second queue to the end of first queue.
////////////////////////////////////////////////*/
template<typename Q1, typename Q2>
struct QueueMerge;

template<typename... Q1, typename... Q2>
struct QueueMerge<Queue<Q1...>, Queue<Q2...>>
{
    using type = Queue<Q1...,Q2...>;
};

template<typename Q1, typename Q2>
using QueueMerge_t = typename QueueMerge<Q1, Q2>::type;

Note: If partial specialization and variadic templates are unfamiliar, it’s worth revisiting those topics first. A detailed explanation is outside the scope of this article.

Price Level Abstration

A price level consists of:

  • A fixed price
  • A queue of orders at that price, ordered by time priority
template <int Price, typename OrderQueue>
struct Level
{
    static constexpr int price = Price;
    using orderQueue = OrderQueue;
};

template<int Price>
using EmptyLevel = Level<Price, Queue<>>;

This gives us a simple compile-time representation of a price level.

Adding an Order to a Level

Adding an order means appending it to the end of the queue.

template <typename Level, typename Order>
struct AddOrderToLevel
{
    using _newQueue = QueuePush_t<typename Level::orderQueue, Order>;
    using type = ::Level<Level::price, _newQueue>;
};

template<typename Level, typename Order>
using AddOrderToLevel_t = typename AddOrderToLevel<Level, Order>::type;

Breaking intermediate results into named aliases (like _newQueue) greatly improves readability in TMP-heavy code. You can think of them as variables in a functional program.

A Subtle but Important TMP Detail

You might have noticed the use of both ::Level<...> and Level::<field> in the previous section. At first glance, this syntax can look confusing — and that’s completely understandable.

You might reasonably expect the following to work:

using type = ::Level<Level::price, _newQueue>;

However, compiling this results in the following error:

error: 'Level' is not a template
   29 |     using type = Level<Level::price, _newQueue>;
      |                  ^~~~~

Why? Within this scope:

  • Level refers to the type passed as a template argument
  • It does not refer to the Level template itself

When the compiler encounters Level<...>, it tries to treat Level as a template. But in this context, Level is already a concrete type, so instantiating it as a template is invalid.

This distinction — between a template name and a type name — is subtle and easy to miss, especially in TMP-heavy code. It’s worth re-reading this carefully; almost everyone trips over this at least once.

Now the question is how do we disambiguate and tell the compiler where to expect a type and where to expect a template.

How Do We Disambiguate?

One way to achieve this is to change the name of the template argument.

template<int OrderId, typename L>
struct RemoveOrderFromLevel
{
    using type = Level<L::price, RemoveOrderFromQueue_t<OrderId, typename L::orderQueue>>;
};

This works because Level now clearly refers to the template, and L refers to the type. However, renaming template parameters like this is not ideal. When writing generic code, it’s often clearer and safer to name parameters after the concepts they represent.

Global Scope Resolution (::)

A better and more explicit solution is to use the global scope resolution operator ::.

Here’s what this tells the compiler:

  • ::Level<...> → instantiate the Level template from the global scope
  • Level::price → access a member of the Level type passed as a template argument

Once you internalize this distinction, the pattern becomes very natural — and you’ll start spotting it throughout advanced TMP code and the standard library itself.

Removing an Order from a Level

Order removal is more interesting.

Unlike a regular queue, we must be able to remove an element from an arbitrary position based on orderId.

Removing an Order from the Queue

template<int OrderId, typename Queue>
struct RemoveOrderFromQueue;

template<int OrderId>
struct RemoveOrderFromQueue<OrderId, Queue<>>
{
    using type = Queue<>;
};

template<int OrderId, typename O1, typename... O2toN>
struct RemoveOrderFromQueue<OrderId, Queue<O1, O2toN...>>
{
    using remainingType =
        typename RemoveOrderFromQueue<OrderId, Queue<O2toN...>>::type;

    using type = std::conditional_t<
        O1::orderId == OrderId,
        remainingType,
        QueueMerge_t<Queue<O1>, remainingType>
    >;
};

template<int OrderId, typename Queue>
using RemoveOrderFromQueue_t =
    typename RemoveOrderFromQueue<OrderId, Queue>::type;

This is a classic recursive template algorithm:

  1. Process the queue element by element
  2. Recursively compute the remaining queue
  3. Use std::conditional_t to decide:
  • Skip the current order if it matches
  • Otherwise, prepend it back

This pattern — recurse, then conditionally rebuild — appears repeatedly in TMP.

Removing an Order from a Level

template<int OrderId, typename Level>
struct RemoveOrderFromLevel
{
    using type = ::Level<
        Level::price,
        RemoveOrderFromQueue_t<OrderId, typename Level::orderQueue>
    >;
};

template<int OrderId, typename Level>
using RemoveOrderFromLevel_t =
    typename RemoveOrderFromLevel<OrderId, Level>::type;

Notice the use of ::Level and Level::<field> at appropriate places.

Key Takeaways

  • Compile-time queues are naturally expressed using variadic templates
  • Recursive TMP often follows a process → recurse → rebuild pattern
  • std::conditional_t is the TMP equivalent of if / else
  • Template parameters and templates themselves live in different namespaces
  • :: is a powerful tool for disambiguation in complex TMP code

What’s Next

With price levels implemented, we can now:

  • Create levels
  • Add and cancel orders while preserving time priority

In Part 4, we will build a ListOfLevels abstraction that:

  • Maintains levels sorted by price
  • Supports insertion and removal
  • Implements a compile-time merge sort

It’s one of the most interesting parts of the series — and likely not something you’ve encountered before.

Implementing merge sort entirely at compile time pushes your understanding of C++ templates well beyond what’s expected in most C++ interviews.

Part 4 — Merge Sort and List of Price Levels


메타데이터
post_id
1908e6f10cb3
slug
compile-time-limit-order-book-in-c-price-levels-and-compile-time-queues-part-3-1908e6f10cb3
url
https://medium.com/@rishabhgarg108/compile-time-limit-order-book-in-c-price-levels-and-compile-time-queues-part-3-1908e6f10cb3
canonical_url
https://medium.com/@rishabhgarg108/compile-time-limit-order-book-in-c-price-levels-and-compile-time-queues-part-3-1908e6f10cb3
author_url
https://medium.com/@rishabhgarg108
status
ok
fetched_at
2026-08-22 20:24:00