C++ for Quants
  • Home
  • News
  • Contact
  • About
Category:

Interview

Order Book C++
Data StructuresInterviewPerformance

Data Structures for Order Book: std::map, std::unordered_map, or std::vector?

by cppforquants July 12, 2026

Ask a C++ developer to design a limit order book, and you will almost always get the same answer: std::map<Price, Level>. It is the answer every tutorial gives, the answer that passes the coding screen, and the answer that feels obviously right: an order book is a collection of price levels that must stay sorted, and std::map is the sorted container. Case closed.

Except that if you look at how production trading systems are actually built, you will struggle to find a red-black tree anywhere near the hot path. The container that “obviously” fits the problem is quietly absent from the systems where the problem matters most.

This article explains why, by putting three candidates through the same test: std::unordered_map, std::map, and — the one nobody suggests in interviews — std::vector.


1.Refresher on std::map, std::unordered_map and std::vectors

What are maps?

A std::map is a sorted associative container storing key-value pairs with unique keys, typically implemented as a red-black tree.

Lookups, insertions, and deletions are all O(log n), and iterating gives you elements in key order.

A good summary video:

What are unordered maps?

A std::unordered_map is a hash table storing key-value pairs with unique keys but no ordering guarantee.

Average O(1) lookup, insert, and erase, degrading to O(n) in the worst case (hash collisions).

The trade-offs: it needs a hash function for the key type, iteration order is unspecified, and the node-based bucket implementation means pointer chasing that can hurt cache performance — which is why HFT code often reaches for open-addressing alternatives like absl::flat_hash_map.

What are vectors?


std::vector is C++’s dynamic array: it stores elements in contiguous memory, gives fast indexed access, and automatically grows when you add more elements.

How to use them to create an order book? But, by the way, what’s an order book?



2. Refresher on order books

An order book is the mechanism that allows a market to match buyers and sellers.

A trader can send:

Buy 100 shares at 99.98
Sell 200 shares at 100.02
Buy 50 shares at market
Cancel order #123
Modify order #456

The exchange maintains the book and decides what happens next.

An order book is the live list of buyers and sellers for a financial instrument.

At first, it is often shown as a table:

Bid SizeBid PriceAsk PriceAsk Size
500 99.98 100.02 300
1,200 99.97 100.03 700
800 99.96 100.04 1,500

The bid side represents buyers.
The ask side represents sellers.

Here, “size” means quantity.

So:

500 at 99.98 means buyers want to buy 500 shares at 99.98.
300 at 100.02 means sellers want to sell 300 shares at 100.02.

Each row is also called a price level.

Level 1 is the best available price on each side:

Level 1 bid = 99.98 x 500
Level 1 ask = 100.02 x 300

Level 2 is the next best price:

Level 2 bid = 99.97 x 1,200
Level 2 ask = 100.03 x 700

Level 3 is the next one after that:

Level 3 bid = 99.96 x 800
Level 3 ask = 100.04 x 1,500

A more natural way to visualize the book is vertically:

            ASK SIDE

Level 3     100.04 x 1,500
Level 2     100.03 x 700
Level 1     100.02 x 300     <- best ask

            spread = 0.04

Level 1      99.98 x 500     <- best bid
Level 2      99.97 x 1,200
Level 3      99.96 x 800

            BID SIDE

The best bid is the highest price someone is willing to buy at.
The best ask is the lowest price someone is willing to sell at.

3. Model an Order Book in C++: Various Attempts

Attempt 1: hash the price levels with std::unordered_map

The message flow suggests an obvious first design. Cancels dominate, and cancels are lookups — so we optimize for lookup. A hash map from price to level gives us O(1) access to any level, and std::unordered_map is sitting right there in the standard library:

cpp

std::unordered_map<Price, Level> bids_;
std::unordered_map<Price, Level> asks_;

Adds are O(1). Cancels are O(1). Modifies are O(1). On paper we’ve made the dominant operations constant-time, and for a few minutes this feels like a solved problem.

Then we implement best_bid().

There is no “first element” in a hash map. Hashing deliberately destroys ordering — that’s what makes it fast — so the only way to find the highest bid is to scan every populated level. The book’s single most frequent query, the one strategy code calls on effectively every tick, has become O(n) over the entire side. Depth walks are worse: there’s no notion of “the next level down” at all; we’d re-scan or sort on demand.

We didn’t build a slow order book. We built something that structurally isn’t an order book. An order book’s defining property is that its levels are ordered — it’s in the name — and we chose the one container whose entire design premise is discarding order. The lookup speed was real, but we optimized the operation that was never going to be the bottleneck and broke the one that defines the product.

Attempt 2: the tree with std::map

So ordering is non-negotiable. The standard library’s ordered associative container is std::map, a red-black tree, and it fixes everything the hash map broke:

std::map<Price, Level, std::greater<Price>> bids_;  // begin() is best bid
std::map<Price, Level> asks_;                        // begin() is best ask

Best bid is bids_.begin() — O(1). Depth walks are in-order traversal. Adds, cancels, and modifies are O(log n), and with a few hundred populated levels, log n is under ten comparisons. This is the textbook answer, it’s correct, and it’s what most order book implementations you’ll find online actually use.

Now replay a day of market data through it and watch what the hardware does.

A full implementation of that version that would make you pass the quant interview in C++ has been proposed in our first article on order books:

using OrderId   = uint64_t;
using Qty       = int64_t;        // signed for partial fills math
using Px       = int64_t;         // price in ticks
enum Side { Buy, Sell };

struct Order {
  OrderId id;
  Side side;
  Px price;
  Qty qty;            // remaining
  uint64_t ts;        // exchange/seq time for tie-breaks
  // intrusive list pointers for O(1) erase
  Order* prev = nullptr;
  Order* next = nullptr;
};

struct Level {
  Px price;
  Order* head = nullptr;
  Order* tail = nullptr;
  inline void push_back(Order* o);
  inline void erase(Order* o);
  bool empty() const { return head == nullptr; }
};

// price → level; bids need descending, asks ascending
using BookSide = std::map<Px, Level, std::greater<Px>>;     // bids
using BookSideAsk = std::map<Px, Level, std::less<Px>>;     // asks

struct OrderBook {
  BookSide bids;
  BookSideAsk asks;
  std::unordered_map<OrderId, Order*> by_id;  // direct handle for cancel/replace

  // API
  void add_limit(OrderId id, Side side, Px px, Qty qty, uint64_t ts);
  void cancel(OrderId id);
  void replace(OrderId id, Px new_px, Qty new_qty, uint64_t ts); // cancel+add semantics
  void match_market(Side side, Qty qty);
  // helpers
  Level& level(BookSide& s, Px px);
  Level& level(BookSideAsk& s, Px px);
};

Attempt 3: keep it sorted, make it contiguous

If pointer chasing is the disease, contiguity is the cure. A sorted std::vector<Level> with binary search keeps the ordering guarantee but lays every level out in a single flat allocation:

cpp

std::vector<Level> bids_;  // sorted; back() is best bid

Lookup is std::lower_bound — still O(log n) comparisons, but now the search touches a handful of cache lines in one array instead of five scattered heap nodes, and the hardware prefetcher can see where we’re going. Best bid is back(). Insertion of a new level requires shifting elements — O(n) in theory — but here the workload rescues us: adds cluster near the touch, so if the vector is sorted with the best price at the back, the memmove is almost always a few elements. In practice this structure embarrasses the tree on a realistic feed.

And yet, benchmark it honestly and there’s a residue we can’t scrub out. Every operation still begins with a search — a binary search is a series of dependent loads, each one’s address unknown until the previous compare resolves, so the pipeline stalls on each step. We’ve made searching cheap. We haven’t asked whether we need to search at all.

Attempt 4: stop searching

Every structure so far has treated the price as an opaque key — something to hash, compare, or binary-search for. But a price in a limit order book is none of those things. Exchanges don’t accept arbitrary prices: every instrument trades on a fixed grid, an integer number of ticks. Two consecutive price levels don’t just happen to be close — they differ by exactly one tick, always. That’s not a statistical tendency we can exploit; it’s a hard constraint the venue enforces on every order.

Once you see prices as grid positions rather than keys, the search problem dissolves. If we know the tick size and pick an anchor price for index zero, then the level for any price isn’t something we find — it’s something we compute.

index = (price − anchor) / tick

One subtraction, one division by a constant, one indexed load into a flat array. No hash function, no comparisons, no dependent loads waiting on the previous step to resolve. The lookup that cost the tree five scattered pointer dereferences and cost the sorted vector a pipeline-stalling binary search is now cheaper than either structure’s first step.

4. A Proposition of Implementation for Attempt 4

In the approach we’re presenting, prices are ticks on a fixed grid, so the book is a flat array of price levels where a price is converted to an index by one subtraction — no hashing, no tree, no search — with a cached best index and each level holding a FIFO of pool-allocated orders linked intrusively.

Adds, cancels, and executions each cost an arithmetic index or an O(1) ID lookup plus an O(1) unlink, touching two or three cache lines and zero heap allocations on the hot path.

Let’s start with an order_book.hpp:

// order_book.hpp — tick-indexed limit order book
//
// Design (see article): contiguous array of price levels indexed by tick
// offset, intrusive doubly-linked FIFO per level, pre-allocated order pool,
// open-addressing map for OrderId -> Order*. Zero heap allocation on the
// hot path after construction.
//
// Conventions:
//   - Price is an integer number of ticks (scale at the feed decoder).
//   - OrderId 0 and ~0 are reserved (empty / tombstone sentinels in IdMap).

#pragma once

#include <cassert>
#include <cstddef>
#include <cstdint>
#include <vector>

using Price   = std::int64_t;
using Qty     = std::int64_t;
using OrderId = std::uint64_t;

enum class Side : std::uint8_t { Bid, Ask };

// ---------------------------------------------------------------------------
// Order: 64 bytes, cache-line aligned. prev/next are intrusive — the order
// *is* its own list node, so joining/leaving a level allocates nothing.
// ---------------------------------------------------------------------------
struct alignas(64) Order {
    OrderId id;
    Qty     qty;
    Price   price;
    Side    side;
    Order*  prev;
    Order*  next;
};
static_assert(sizeof(Order) == 64, "one order per cache line");

// ---------------------------------------------------------------------------
// OrderPool: all orders live in one contiguous slab allocated at startup.
// alloc/release are a free-list push/pop — no new/delete on the hot path.
// ---------------------------------------------------------------------------
class OrderPool {
public:
    explicit OrderPool(std::size_t capacity) : slots_(capacity) {
        free_.reserve(capacity);
        for (std::size_t i = capacity; i-- > 0;)
            free_.push_back(&slots_[i]);
    }

    Order* alloc() {
        assert(!free_.empty() && "pool exhausted — size it to the session");
        Order* o = free_.back();
        free_.pop_back();
        return o;
    }

    void release(Order* o) { free_.push_back(o); }

private:
    std::vector<Order>  slots_;
    std::vector<Order*> free_;
};

// ---------------------------------------------------------------------------
// Level: FIFO queue of resting orders at one price. head is oldest (first to
// fill), tail is where adds append — price-time priority falls out of the
// list order. total_qty is maintained incrementally so top-of-book snapshots
// never walk the list.
// ---------------------------------------------------------------------------
struct Level {
    Order* head      = nullptr;
    Order* tail      = nullptr;
    Qty    total_qty = 0;

    bool empty() const { return head == nullptr; }

    void push_back(Order* o) {
        o->prev = tail;
        o->next = nullptr;
        if (tail) tail->next = o; else head = o;
        tail = o;
        total_qty += o->qty;
    }

    // O(1) given the order pointer — no search. This is why cancels, the
    // dominant message type, stay cheap.
    void unlink(Order* o) {
        if (o->prev) o->prev->next = o->next; else head = o->next;
        if (o->next) o->next->prev = o->prev; else tail = o->prev;
        total_qty -= o->qty;
    }
};

// ---------------------------------------------------------------------------
// BookSide: the tick-indexed array. Price -> level is one subtraction and
// one indexed load; no hashing, no comparisons, no pointer chasing.
// best_ caches the top of book; when the top level empties we scan linearly
// toward the interior — adjacent prices are adjacent cache lines, and the
// next populated level is almost always within a few ticks.
// ---------------------------------------------------------------------------
template <bool IsBid>
class BookSide {
public:
    static constexpr std::size_t kLevels = std::size_t{1} << 16;
    static constexpr std::size_t kNone   = ~std::size_t{0};

    explicit BookSide(Price anchor)
        : levels_(kLevels), anchor_(anchor) {}

    void add(Order* o) {
        const std::size_t idx = index(o->price);
        levels_[idx].push_back(o);
        if (best_ == kNone || better(idx, best_)) best_ = idx;
    }

    void remove(Order* o) {
        const std::size_t idx = index(o->price);
        levels_[idx].unlink(o);
        if (idx == best_ && levels_[idx].empty()) advance_best();
    }

    void reduce(Order* o, Qty by) {
        o->qty -= by;
        levels_[index(o->price)].total_qty -= by;
    }

    bool  empty()      const { return best_ == kNone; }
    Price best_price() const { return anchor_ + static_cast<Price>(best_); }
    Qty   best_qty()   const { return levels_[best_].total_qty; }
    const Order* best_order() const { return levels_[best_].head; }

private:
    std::size_t index(Price p) const {
        assert(p >= anchor_ &&
               static_cast<std::size_t>(p - anchor_) < kLevels &&
               "price outside book range — re-anchor on the slow path");
        return static_cast<std::size_t>(p - anchor_);
    }

    static bool better(std::size_t a, std::size_t b) {
        if constexpr (IsBid) return a > b;   // best bid = highest price
        else                 return a < b;   // best ask = lowest price
    }

    void advance_best() {
        if constexpr (IsBid) {
            while (best_ != 0) {
                --best_;
                if (!levels_[best_].empty()) return;
            }
        } else {
            while (best_ + 1 < kLevels) {
                ++best_;
                if (!levels_[best_].empty()) return;
            }
        }
        best_ = kNone;   // side is empty
    }

    std::vector<Level> levels_;   // one flat allocation, made once
    Price              anchor_;   // price at index 0
    std::size_t        best_ = kNone;
};

// ---------------------------------------------------------------------------
// IdMap: OrderId -> Order*, open addressing with linear probing. Flat slot
// array — one hash, then a short contiguous probe. Sized 2x expected load
// at construction; erases leave tombstones (fine for a trading session,
// rebuild between sessions if reusing).
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// IdMap: OrderId -> Order*, open addressing with linear probing. Flat slot
// array — one hash, then a short contiguous probe. Sized 2x expected load
// at construction; erases leave tombstones (fine for a trading session,
// rebuild between sessions if reusing).
// ---------------------------------------------------------------------------
class IdMap {
public:
    explicit IdMap(std::size_t expected) {
        std::size_t cap = 1;
        while (cap < expected * 2) cap <<= 1;
        slots_.assign(cap, Slot{kEmpty, nullptr});
        mask_ = cap - 1;
    }

    void insert(OrderId id, Order* o) {
        std::size_t i = hash(id) & mask_;
        while (slots_[i].key != kEmpty && slots_[i].key != kTomb)
            i = (i + 1) & mask_;
        slots_[i] = Slot{id, o};
    }

    Order* find(OrderId id) const {
        std::size_t i = hash(id) & mask_;
        while (slots_[i].key != kEmpty) {
            if (slots_[i].key == id) return slots_[i].val;
            i = (i + 1) & mask_;
        }
        return nullptr;
    }

    void erase(OrderId id) {
        std::size_t i = hash(id) & mask_;
        while (slots_[i].key != kEmpty) {
            if (slots_[i].key == id) {
                slots_[i].key = kTomb;
                slots_[i].val = nullptr;
                return;
            }
            i = (i + 1) & mask_;
        }
    }

private:
    static constexpr OrderId kEmpty = 0;
    static constexpr OrderId kTomb  = ~OrderId{0};

    struct Slot { OrderId key; Order* val; };

    static std::size_t hash(OrderId id) {   // splitmix64 finalizer
        std::uint64_t x = id;
        x ^= x >> 33; x *= 0xff51afd7ed558ccdULL;
        x ^= x >> 33; x *= 0xc4ceb9fe1a85ec53ULL;
        x ^= x >> 33;
        return static_cast<std::size_t>(x);
    }

    std::vector<Slot> slots_;
    std::size_t       mask_;
};

// ---------------------------------------------------------------------------
// OrderBook: ties the pieces together. The three feed-driven mutations map
// onto it directly:
//   Add     -> pool alloc, arithmetic index, list append   (0 allocations)
//   Cancel  -> id lookup, O(1) unlink, pool release        (0 allocations)
//   Execute -> id lookup, reduce or remove                 (0 allocations)
// ---------------------------------------------------------------------------
class OrderBook {
public:
    struct Quote { Price price; Qty qty; bool valid; };

    OrderBook(Price anchor, std::size_t max_live_orders)
        : bids_(anchor), asks_(anchor),
          pool_(max_live_orders), ids_(max_live_orders) {}

    void add(OrderId id, Side side, Price price, Qty qty) {
        Order* o = pool_.alloc();
        *o = Order{id, qty, price, side, nullptr, nullptr};
        if (side == Side::Bid) bids_.add(o); else asks_.add(o);
        ids_.insert(id, o);
    }

    void cancel(OrderId id) {
        if (Order* o = ids_.find(id)) remove(o);
    }

    // Execution reported by the feed against a resting order.
    void execute(OrderId id, Qty exec_qty) {
        Order* o = ids_.find(id);
        if (!o) return;
        if (exec_qty >= o->qty) {
            remove(o);
        } else if (o->side == Side::Bid) {
            bids_.reduce(o, exec_qty);
        } else {
            asks_.reduce(o, exec_qty);
        }
    }

    Quote best_bid() const {
        return bids_.empty() ? Quote{0, 0, false}
                             : Quote{bids_.best_price(), bids_.best_qty(), true};
    }

    Quote best_ask() const {
        return asks_.empty() ? Quote{0, 0, false}
                             : Quote{asks_.best_price(), asks_.best_qty(), true};
    }

private:
    void remove(Order* o) {
        if (o->side == Side::Bid) bids_.remove(o); else asks_.remove(o);
        ids_.erase(o->id);
        pool_.release(o);
    }

    BookSide<true>  bids_;
    BookSide<false> asks_;
    OrderPool       pool_;
    IdMap           ids_;
};

How to test this approach?

Create a demo.cpp:

#include "order_book.hpp"
#include <cstdio>

static void print_top(const OrderBook& book, const char* tag) {
    auto b = book.best_bid();
    auto a = book.best_ask();
    std::printf("%-28s  bid: ", tag);
    if (b.valid) std::printf("%lld x %lld", (long long)b.qty, (long long)b.price);
    else         std::printf("--");
    std::printf("   ask: ");
    if (a.valid) std::printf("%lld x %lld", (long long)a.qty, (long long)a.price);
    else         std::printf("--");
    std::printf("\n");
}

int main() {
    // Anchor at tick 10'000, capacity for 1M live orders.
    OrderBook book(/*anchor=*/10'000, /*max_live_orders=*/1'000'000);

    book.add(1, Side::Bid, 10'100, 500);
    book.add(2, Side::Bid, 10'101, 300);   // better bid
    book.add(3, Side::Bid, 10'101, 200);   // joins queue behind id 2
    book.add(4, Side::Ask, 10'103, 400);
    book.add(5, Side::Ask, 10'102, 250);   // better ask
    print_top(book, "after adds");

    book.cancel(2);                        // partial drain of best bid level
    print_top(book, "cancel id 2");

    book.cancel(3);                        // best bid level empties -> scan inward
    print_top(book, "cancel id 3");

    book.execute(5, 100);                  // partial execution at best ask
    print_top(book, "execute 100 vs id 5");

    book.execute(5, 150);                  // fills remainder -> level empties
    print_top(book, "execute 150 vs id 5");

    book.cancel(1);
    book.cancel(4);
    print_top(book, "book emptied");

    return 0;
}

Let’s compile and run:

g++ -std=c++20 -O2 -Wall -Wextra -o demo demo.cpp && ./demo

Which gives:

after adds                    bid: 500 x 10101   ask: 250 x 10102
cancel id 2                   bid: 200 x 10101   ask: 250 x 10102
cancel id 3                   bid: 500 x 10100   ask: 250 x 10102
execute 100 vs id 5           bid: 500 x 10100   ask: 150 x 10102
execute 150 vs id 5           bid: 500 x 10100   ask: 400 x 10103
book emptied                  bid: --   ask: --
July 12, 2026 0 comments
top shared_ptr questions
InterviewPerformance

C++ Shared Pointers: Top shared_ptr Quant Interview Questions

by cppforquants November 30, 2025

C++ shared pointers come up again and again in quant interviews, and for good reason: they sit at the intersection of memory management, performance, ownership semantics, and real-time system reliability, all skills quants are expected to master. In modern C++ codebases used across trading desks, risk engines, and pricing libraries, std::shared_ptr is everywhere, yet many candidates only know the surface-level behavior. Interviewers use shared pointer questions to test whether you understand what’s really happening under the hood: control blocks, atomic reference counting, cache effects, and the subtle performance pitfalls that matter in low-latency environments. They also want to see if you can reason about ownership graphs, detect leaks caused by cycles, and choose correctly between shared_ptr, unique_ptr, and raw pointers in high-frequency workloads. What are the top shared_ptr questions?

Question 1: “What is a Shared Pointer? Give A Quantitative Finance Example”

A shared_ptr is a reference-counted smart pointer that enables shared ownership of a dynamically allocated object, automatically deleting it when the last owner goes away.

In many pricing engines, several components need access to the same yield-curve snapshot without copying it. A shared_ptr is ideal here because it lets each module share ownership safely. Here’s a minimal example:

#include <iostream>
#include <memory>
#include <vector>

struct YieldCurve {
    std::vector<double> tenors;
    std::vector<double> discountFactors;

    YieldCurve() {
        std::cout << "YieldCurve built\n";
    }
    ~YieldCurve() {
        std::cout << "YieldCurve destroyed\n";
    }
};

int main() {
    auto curve = std::make_shared<YieldCurve>();

    std::cout << "Ref count initially: " << curve.use_count() << "\n";

    {
        // Risk model shares the same curve
        auto riskModelCurve = curve;
        std::cout << "Ref count after risk model uses it: "
                  << curve.use_count() << "\n";
    } // riskModelCurve dies, curve stays alive

    std::cout << "Ref count after model finished: "
              << curve.use_count() << "\n";
}

What This Example Demonstrates

1. Shared Ownership of a Core Market Object

In real pricing systems, many components—pricing engines, risk calculators, scenario generators—must all access the same yield curve. Using std::shared_ptr ensures the curve persists as long as at least one module still uses it, without forcing expensive deep copies.

2. Reference Counting Behind the Scenes

Each time the shared_ptr is copied (e.g., when the risk model takes a reference), the strong reference count increases. When copies go out of scope, the count decreases. Only when the count reaches zero does the object get destroyed. This is exactly what happens to the YieldCurve instance across scopes in the example.

3. Automatic Lifetime Management (RAII)

You never call delete on the yield curve. Its lifetime is tied to the lifetime of the owning shared_ptr instances.
This reduces the classic risks in large quant codebases: dangling pointers, double deletes, and lifetime mismatches between pricing components.

Question 2: “How does shared_ptr manage reference counting?“

std::shared_ptr uses a separate control block to track how many owners an object has. Every time a shared_ptr is copied, the control block increments a strong reference count. Every time a shared_ptr is destroyed or reset, that count is decremented. When the strong count reaches zero, the managed object is automatically deleted.

Under the hood, the control block stores:

  • A strong reference count
    (number of active shared_ptr owning the object)
  • A weak reference count
    (number of weak_ptr observing the object)
  • The managed pointer
  • (Optionally) a custom deleter and allocator

All reference count updates are atomic, which makes shared_ptr safe to use across threads—though more expensive than unique_ptr. In practice, this mechanism ensures that shared market objects (like yield curves, volatility surfaces, or trade graphs) live exactly as long as the last component using them, with no need for manual delete and no risk of premature destruction. One of the top shared_ptr questions!

Question 3: “What causes a memory leak with shared_ptr?“

A memory leak with std::shared_ptr happens when two or more objects form a cyclic reference, meaning each holds a shared_ptr to the other, so their reference counts never drop to zero and their destructors never run.

For example, if struct A has std::shared_ptr<B> b; and struct B has std::shared_ptr<A> a;, creating the cycle a->b = b; and b->a = a; will leak both objects because each keeps the other alive. The fix is to use std::weak_ptr on one side of the relationship.

struct B;

struct A { std::shared_ptr<B> b; };
struct B { std::shared_ptr<A> a; }; // ← this creates a cycle and leaks

auto a = std::make_shared<A>();
auto b = std::make_shared<B>();
a->b = b;
b->a = a; // reference counts never reach 0 → leak

Here’s the same idea but fixed using std::weak_ptr so the cycle doesn’t keep the objects alive:

#include <memory>
#include <iostream>

struct B;

struct A {
    std::shared_ptr<B> b;
    ~A() { std::cout << "A destroyed\n"; }
};

struct B {
    std::weak_ptr<A> a;  // weak_ptr breaks the cycle
    ~B() { std::cout << "B destroyed\n"; }
};

int main() {
    auto a = std::make_shared<A>();
    auto b = std::make_shared<B>();

    a->b = b;
    b->a = a;  // does NOT increase refcount

    return 0;  // both A and B are destroyed normally
}
That's one of the top shared_ptr questions.

Question 4: make_shared vs. shared_ptr<T>(new T): what’s the difference?

std::make_shared<T>() and std::shared_ptr<T>(new T) both create a shared_ptr, but they differ in performance, memory layout, and exception-safety:

  • make_shared is faster and uses one allocation: it allocates the control block and the object in a single heap allocation, improving cache locality.
  • shared_ptr<T>(new T) uses two allocations: one for the control block and one for the object, making it slower and more memory hungry.
  • make_shared is exception-safe: if constructor arguments throw, no memory is leaked. With shared_ptr<T>(new T), if you add custom deleters or wrap logic incorrectly, leaks can occur.
  • make_shared is preferred except when you need a custom deleter or want separate lifetimes for control block and object (rare—e.g., weak-to-shared resurrection edge cases).

Example:

auto p1 = std::make_shared<MyObject>();        // one allocation, safe
auto p2 = std::shared_ptr<MyObject>(new MyObject());  // two allocations

Question 5: Why is shared_ptr slower?

std::shared_ptr is slower because it performs atomic reference counting, extra bookkeeping, and sometimes extra allocations to manage shared ownership. Every copy of a shared_ptr must atomically increment the control block’s reference count, and every destruction must atomically decrement it; these atomic operations create contention, inhibit compiler optimizations, and add CPU overhead. A shared_ptr also maintains both a strong and weak count, uses a control block to track them, and may require separate heap allocations (unless created via make_shared). This combination of atomic ops + bookkeeping + heap activity makes shared_ptr significantly slower than a raw pointer or even a unique_ptr, which performs no reference counting at all.

November 30, 2025 0 comments
how to become a quantitative developer
InterviewJobs

How To Become a Quantitative Developer?

by cppforquants November 1, 2025

When I started my journey in quantitative development, I didn’t have a clear roadmap: just curiosity and a love for both code and markets. I was a Python machine learning engineer for years before realizing that the intersection of finance, mathematics, and software engineering was where my mind truly thrived. Over time, I transitioned from real-time ML systems to building pricing models, working with risk engines, and optimizing massive data pipelines: the kind of problems that sit at the heart of quantitative finance. So, how to become a quantitative developer?

If you’re reading this, you might be standing at a similar crossroads. Maybe you’re a developer who’s intrigued by trading systems and quantitative models. Maybe you’re a mathematician who wants to turn theory into production code. Or maybe, like me, you simply want to understand how raw market data transforms into the numbers driving billion-dollar decisions.

Becoming a quantitative developer is about learning how to think like both a scientist and an engineer, balancing mathematical rigor with system design and performance awareness. In this article, I’ll break down what it takes to enter the field, what quantitative skills truly matter, and how to build the kind of mindset that thrives in quant environments from investment banks to hedge funds and prop shops.

1. The Classic Path: Study Computer Science and Finance

The traditional route into quantitative development starts with a strong foundation in computer science, mathematics, and finance. Many professionals follow this academic path through degrees in computer science or applied mathematics, often complemented by a master’s in financial engineering or quantitative finance from institutions such as Imperial College London, ETH Zürich, Carnegie Mellon University, or Université Paris-Saclay. How to become a quantitative developer? Generally speaking, when you’re playing that path, try to shoot a university in that group:

Mathematics matter, so choose your major carefully. I’m biased because it’s what I studied but without it: you will plateau. Why? Watch this:

Take Max, for example — a computer science graduate from ETH who later completed a Master’s in Financial Engineering at Imperial. During his studies, he learned to build valuation models in Python, write C++ for high-performance simulations, and understand the pricing mechanics of derivatives and bonds. By the time he graduated, he could not only optimize a sorting algorithm but also explain what a convexity adjustment meant in a swap curve — and that combination is gold in the quant world.

2. The Pirate Path: Get Any Job and Move Towards Quantitative Finance

But you can do it differently. Not like Max.

Maybe you didn’t go to Imperial or ETH. Maybe your degree wasn’t in quantitative finance — or you didn’t even study finance at all. That’s fine. The truth is, a lot of successful quantitative developers didn’t follow the “textbook” path. They hacked their way into it: from software engineering, data science, or even DevOps.

I call this the Pirate Path.

You start by getting any strong engineering job: one that forces you to write production-grade code, ship systems, and solve real-world latency or data problems. You learn to build, debug, and scale things that matter. Then, little by little, you sail toward finance: you read about markets, build small analytics projects on the side, or start automating something related to trading data.

You will need to learn little by little about every domain linked to quant engineering and it’s best if you it in each job you move to:

This requires to move job strategically to collect skillsets like mushrooms in Mario. Build the map and check the boxes.

Little by little, apply to more and more of those companies:

3. The Self-Made Path: Produce Content About it and Get Noticed

Then there’s the third way: the one that didn’t really exist twenty years ago but is now reshaping how people break into the quant world. The Self-Made Path.

This is the route of the builder-creator hybrid. You don’t just learn, you document what you learn.

You share your experiments, your insights, and your projects with the world. Write threads, blog posts, tutorials, or publish open-source tools. The more you share, the more you attract people who think like you… and sooner or later, that visibility opens real doors.

That’s how I ended up connecting with many in the quant and fintech world: by writing and publishing regularly. Whether it was explaining pricing models, exploring real-time ML systems, or diving into C++ performance topics, I was simply trying to make sense of my own work in public. But what started as writing for myself ended up building credibility, network, and opportunity: the things that often matter more than a title or diploma.

The Self-Made Path rewards curiosity, consistency, and clarity. If you can explain complex financial or technical concepts in a way that others understand, you’re already thinking like a quant developer because that’s exactly what we do: take something abstract and make it precise, testable, and real.

So if you’re just getting started, don’t wait for permission. Start writing. Record videos. Open-source your tools. People will notice. And when they do, the line between student and quant developer starts to blur very quickly.

Conclusion

So, how to become a quantitative developer? There isn’t just one way to become a quantitative developer, there are many. Max took the classic route through top universities, guided by structure and theory. The Pirate took detours, learning from production fires, late-night debugging, and messy real-world systems. The Self-Made builder created his own spotlight by sharing what he learned in public.

Each path has its price and its reward.
The Classic Path gives you clarity.
The Pirate Path gives you grit.
The Self-Made Path gives you reach.

What matters most isn’t where you start, but how far you’re willing to keep learning once you’re in motion.
Finance evolves fast. So do programming languages, architectures, and datasets.
The best quantitative developers stay curious, adaptable, and humble, engineers of both code and understanding.

Whether you’re optimizing a pricing engine in C++, experimenting with yield curve fitting, or explaining your latest discovery on a blog, you’re doing the same thing: turning complexity into clarity.

So pick your path. Build. Break things. Learn.
And remember: in this world, curiosity compounds faster than capital.

November 1, 2025 0 comments
Interview question for quant
InterviewPerformance

Top C++ Interview Questions for Quants: Implement LRU Cache

by cppforquants September 14, 2025

One of the most common C++ interview questions for quantitative finance roles is the LRU (Least Recently Used) Cache. It looks simple at first, but it tests a candidate’s ability to design efficient data structures, balance time and space complexity, and leverage the C++ Standard Library effectively. How to solve one of the top C++ interview questions? Let’s dive in!

1. Problem Statement

Design and implement a Least Recently Used (LRU) Cache in C++. The cache should support the following operations:

  1. get(key) → Return the value if the key exists in the cache; otherwise return “not found.” Accessing a key should mark it as the most recently used.
  2. put(key, value) → Insert or update a key-value pair. If the cache exceeds its capacity, it must evict the least recently used item.

Requirements:

  • Both operations should run in O(1) average time complexity.
  • The cache should be limited to a fixed capacity defined at construction.
  • You may assume all keys are unique.
  • Iterators or pointers must remain valid during reordering.
  • The design should be clean, modern C++, using STL where appropriate.

This problem is a classic interview favorite because it tests understanding of hash maps, linked lists, and how to combine data structures for performance-critical systems.

2. Implementation

This is a suggestion of implementation:

#include <list>
#include <unordered_map>
#include <optional>
#include <iostream>
#include <string>

template <class Key, class Value>
class LRUCache {
public:
    explicit LRUCache(std::size_t capacity) : cap_(capacity) {}

    // Return value if present; moves the entry to the front (most-recently used).
    std::optional<Value> get(const Key& key) {
        auto it = idx_.find(key);
        if (it == idx_.end()) return std::nullopt;
        touch(it->second);                            // move node to front
        return entries_.front().second;               // value after touch
    }

    // Insert or update; moves/creates entry as most-recently used.
    void put(const Key& key, const Value& value) {
        auto it = idx_.find(key);
        if (it != idx_.end()) {
            // update value and move to front
            it->second->second = value;
            touch(it->second);
            return;
        }
        // evict if needed
        if (entries_.size() == cap_) {
            const Key& k_evict = entries_.back().first;
            idx_.erase(k_evict);
            entries_.pop_back();
        }
        // emplace new at front
        entries_.emplace_front(key, value);
        idx_[key] = entries_.begin();
    }

    bool contains(const Key& key) const { return idx_.count(key) != 0; }
    std::size_t size() const { return entries_.size(); }

private:
    using Node = std::pair<Key, Value>;
    using List = std::list<Node>;
    using Iter = typename List::iterator;

    void touch(Iter it) {
        // move node to front (MRU)
        entries_.splice(entries_.begin(), entries_, it);
    }

    std::size_t cap_;
    List entries_;                          // front = most-recently used
    std::unordered_map<Key, Iter> idx_;     // key -> node iterator
};


// -----------------------------
// Example main() for testing
// -----------------------------
int main() {
    LRUCache<int, std::string> cache(2);

    cache.put(1, "one");
    cache.put(2, "two");

    if (auto v = cache.get(1)) {
        std::cout << "Get 1: " << *v << "\n";  // prints "one"
    }

    cache.put(3, "three"); // evicts key 2

    if (auto v = cache.get(2)) {
        std::cout << "Get 2: " << *v << "\n";
    } else {
        std::cout << "Get 2: miss\n";          // prints "miss"
    }

    if (auto v = cache.get(3)) {
        std::cout << "Get 3: " << *v << "\n";  // prints "three"
    }

    return 0;
}

The cache is built with two core structures: a std::list to maintain the usage order (most recently used at the front, least at the back), and an unordered_map to allow O(1) access to list nodes. When get is called, we move the accessed node to the front of the list. When put is called, we either update an existing node and move it to the front, or insert a new one. If inserting exceeds the capacity, the node at the back (the least recently used) is evicted. This combination ensures that both operations run in O(1) average time.

3. Compilation and Execution

To compile the code, prepare a CMakeLists.txt:

cmake_minimum_required(VERSION 3.10)
project(lrucache)
set(CMAKE_CXX_STANDARD 17)
add_executable(lrucache ../lrucache.cpp)

and compile via cmake:

mkdir build
cd build
cmake ..
make

Then, you can execute it with:

➜  build git:(main) ✗ ./lrucache 
Get 1: one
Get 2: miss
Get 3: three

4. Access the code on Github

The code is accessible here for you to clone, compile and run with a README file for one of the top C++ interview questions:

https://github.com/cppforquants/lrucache

September 14, 2025 0 comments
non-overlapping intervals in C++
Interview

Non-Overlapping Intervals in C++: A Quantitative Developer Interview Question

by cppforquants July 13, 2025

In quantitative developer interviews, you’re often tested not just on algorithms, but on how they apply to real-world financial systems. One common scenario: cleaning overlapping time intervals from noisy market data feeds. This challenge maps directly to LeetCode 435: Non-Overlapping Intervals. In this article, we’ll solve it in C++ and explore why it matters in quant finance from ensuring data integrity to preparing time series for model training. How to solve the problem of non-overlapping intervals in C++?

Let’s dive into a practical problem that tests both coding skill and quant context.

1. Problem Statement

You’re given a list of intervals, where each interval represents a time range during which a market data feed was active. Some intervals may overlap, resulting in duplicated or conflicting data.

Your task is to determine the minimum number of intervals to remove so that the remaining intervals do not overlap. The goal is to produce a clean, non-overlapping timeline: a common requirement in financial data preprocessing.

Input: intervals = {{1,3}, {2,4}, {3,5}, {6,8}}
Output: 1

Explanation: Removing {2,4} results in non-overlapping intervals: {{1,3}, {3,5}, {6,8}}.

2. The Solution Explained

To remove the fewest intervals and eliminate overlaps, we take a greedy approach: we always keep the interval that ends earliest, since this leaves the most room for future intervals.

By sorting all intervals by their end time, we can iterate through them and:

  • Keep an interval if it doesn’t overlap with the last selected one.
  • Remove it otherwise.

This strategy ensures we keep as many non-overlapping intervals as possible and thus remove the minimum necessary.

What is the time and space complexity for this approach?

Sorting dominates the time complexity. While the actual iteration through the intervals is linear (O(n)), sorting the n intervals takes O(nlogn), and that becomes the bottleneck.

Although we process all n intervals, we do so in place without allocating any extra data structures. We use just a few variables (last_end, removed), so the auxiliary space remains constant. That’s why the space complexity is O(1) assuming the input list is already given and we’re allowed to sort it directly.

AspectValueExplanation
Time ComplexityO(n log n)Due to sorting the intervals by end time
Space ComplexityO(1) (excluding input)Only a few variables used; no extra data structures needed

Now, let’s implement it in C++.

3. A C++ Implementation

Here is a C++ implementation of the solution:

#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;

// Greedy function to compute the minimum number of overlapping intervals to remove
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
    // Sort intervals by end time
    sort(intervals.begin(), intervals.end(), [](const auto& a, const auto& b) {
        return a[1] < b[1];
    });

    int removed = 0;
    int last_end = INT_MIN;

    for (const auto& interval : intervals) {
        if (interval[0] < last_end) {
            // Overlapping — must remov.e this one
            removed++;
        } else {
            // No overlap — update last_end
            last_end = interval[1];
        }
    }

    return removed;
}

int main() {
    vector<vector<int>> intervals = {{1, 3}, {2, 4}, {3, 5}, {6, 8}};
    int result = eraseOverlapIntervals(intervals);
    cout << "Minimum intervals to remove: " << result << endl;
    return 0;
}

Let’s compile:

mkdir build
cmake ..
make

And run the code:

➜  build git:(main) ✗ ./intervals
Minimum intervals to remove: 1

4. Do It Yourself

To do it yourself, you can clone my repository to solve non-overlapping intervals in C++here:

https://github.com/cppforquants/overlappingintervals

Just git clone and run the compilation/execution commands from the Readme:

# Overlapping Intervals

The overlapping intervals problem for quant interviews.

## Build

```bash
mkdir build
cd build
cmake ..
make
```

## Run
```
./intervals

July 13, 2025 0 comments
two sum problem C++
Interview

Hash Maps for Quant Interviews: The Two Sum Problem in C++

by cppforquants June 29, 2025

In the article of today, we talk about an interview question, simple on the surface, that’s given quite a lot as interview question for quants positions: the two sum problem.

What is the two sum problem in C++?

🔍 Problem Statement

Given a list of prices and a target value, return all the indices of price pairs in the list that sum up to the target.

Example:

nums = {2, 7, 11, 15}, target = 9  

Output: [{0, 1}]

// because nums[0] + nums[1] = 2 + 7 = 9


1. The Naive Implementation in [math] O(n^2) [/math] Time Complexity


The simple way of doing is to loop through the list of prices twice to test every possible sums:

std::vector<std::pair<int, int>> twoSum(const std::vector<int>& nums, int target) {
    std::vector<std::pair<int, int>> results = {};
    for (int i = 0; i < nums.size(); ++i) {
        for (int j = i + 1; j < nums.size(); j++) {
            if (nums[i]+nums[j] == target){
                results.push_back({i, j});
            };
        };
    };
    return results;
}

This can be used with the list of prices given in our introduction:


int main() {
    std::vector<int> nums = {2, 7, 11, 15};
    int target = 9;
    auto results = twoSum(nums, target);

    for (const auto& pair: results) {
        std::cout << "Indices: " << pair.first << ", " << pair.second << std::endl;
    };


    return 0;
}

Running the code above prints the solution:

➜  build git:(main) ✗ ./twosum
Indices: 0, 1

The time complexity is [math] O(n^2) [/math] because we’re looping twice over the list of prices.

And that’s it, we’ve nailed the two sum problem in C++!

But…. is it possible to do better?

2. The Optimal Implementation in [math] O(n) [/math] Time Complexity

The optimal way is to use a hash map to store previously seen numbers and their indices, allowing us to check in constant time whether the complement of the current number (i.e. target - current) has already been encountered.

This reduces the time complexity from O(n²) to O(n), making it highly efficient for large inputs.

std::vector<std::pair<int, int>> twoSumOptimized(const std::vector<int>& nums, int target) {
    std::vector<std::pair<int, int>> results = {};
    std::unordered_map<int, int> pricesMap;
    for (int i = 0; i < nums.size(); ++i) {

        int diff = target - nums[i];

        if (pricesMap.find(diff) != pricesMap.end()){
            results.push_back({pricesMap[diff], i});

        } 
        pricesMap[nums[i]] = i;

    };
    return results;
}

And we can run it on the exact same example but this time, it’s O(n) as we loop once and hash maps access/storing time complexity are O(1):


int main() {
    std::vector<int> nums = {2, 7, 11, 15};
    int target = 9;

    auto results = twoSumOptimized(nums, target);

    for (const auto& pair: results) {
        std::cout << "Indices: " << pair.first << ", " << pair.second << std::endl;
    };

    return 0;
}

3. A Zoom on Unordered Hash Maps in C++

In C++, std::unordered_map is the go-to data structure for constant-time lookups. Backed by a hash table, it allows you to insert, search, and delete key-value pairs in average O(1) time. For problems like Two Sum, where you’re checking for complements on the fly, unordered_map is the natural fit.

Here’s a quick comparison with std::map:

Featurestd::unordered_mapstd::map
Underlying StructureHash tableRed-Black tree (balanced BST)
Average Lookup TimeO(1)O(log n)
Worst-case Lookup TimeO(n) (rare)O(log n)
Ordered Iteration❌ No✅ Yes
C++ Standard IntroducedC++11C++98
Typical Use CasesFast lookups, cache, setsSorted data, range queries

Use unordered_map when:

  • You don’t need the keys to be sorted
  • You want maximum performance for insert/lookup
  • Hashing the key type is efficient and safe (e.g. int, std::string)

Let me know if you’d like to add performance tips, custom hash examples, or allocator benchmarks.

The code for the article is available here:

https://github.com/cppforquants/twosumprices

June 29, 2025 0 comments
moving average interview
Interview

Calculate Moving Average in C++ in O(1) – An Interview-Style Problem

by cppforquants April 26, 2025

A classic interview question in quantitative finance or software engineering roles is:
“Design a data structure that calculates the moving average of a stock price stream in O(1) time per update.”.
How to calculate the moving average in C++ in an optimal way?

Let’s tackle this with a focus on Microsoft (MSFT) stock, although the solution is applicable to any time-series financial instrument. We’ll use C++ to build an efficient, clean implementation suitable for production-grade quant systems.

1. Problem Statement

Design a class that efficiently calculates the moving average of the last N stock prices.

Your class should support:

  • void addPrice(double price): Adds the latest price.
  • double getAverage(): Returns the average of the last N prices.

Constraints:

  • The moving average must be updated in O(1) time per price.
  • Handle the case where fewer than N prices have been added.

Implement this in C++.

You will need to complete the following code:

#include <vector>


class MovingAverage {
public:
    explicit MovingAverage(int size);
    void addPrice(double price);
    double getAverage() const;
private:
    std::vector<double> buffer;
    int maxSize;
    double sum = 0.0;
};

Imagine the following historical prices for Microsoft stocks:

DayPrice (USD)
1400.0
2402.5
3405.0
4410.0
5412.0
6415.5

And assume we want to calculate the moving average of prices on 3 days, everyday, as a test.

2. A Naive Implementation in O(N)

Let’s start with a first implementation using `std::accumulate`.

It’s defined as follow in the C++ documentation:
“std::accumulate Computes the sum of the given value init and the elements in the range [first, last)“. The last iterator is not included in the operation. This is a half-open interval, written as.

std::accumulate is O(N) in time complexity.

Let’s use it to calculate the MSTF stock price moving average in C++:

#include <iostream>
#include <vector>
#include <numeric>

class MovingAverage {
public:
    explicit MovingAverage(int size) : maxSize(size) {}

    void addPrice(double price) {
        buffer.push_back(price);
        if (buffer.size() > maxSize) {
            buffer.erase(buffer.begin()); // O(N)
        }
    }

    double getAverage() const {
        if (buffer.empty()) return 0.0;
        double sum = std::accumulate(buffer.begin(), buffer.end(), 0.0); // O(N)
        return sum / buffer.size();
    }

private:
    std::vector<double> buffer;
    int maxSize;
};

int main() {
    MovingAverage ma(3); // 3-day moving average
    std::vector<double> msftPrices = {400.0, 402.5, 405.0, 410.0, 412.0, 415.5};

    for (size_t i = 0; i < msftPrices.size(); ++i) {
        ma.addPrice(msftPrices[i]);
        std::cout << "Day " << i + 1 << " - Price: " << msftPrices[i]
                  << ", 3-day MA: " << ma.getAverage() << std::endl;
    }

    return 0;
}

Every new day the moving average is re-calculated from scratch, not ideal! But it gives the right results:

➜  build ./movingaverage 
Day 1 - Price: 400, 3-day MA: 400
Day 2 - Price: 402.5, 3-day MA: 401.25
Day 3 - Price: 405, 3-day MA: 402.5
Day 4 - Price: 410, 3-day MA: 405.833
Day 5 - Price: 412, 3-day MA: 409
Day 6 - Price: 415.5, 3-day MA: 412.5

3. An optimal Implementation in O(1)

Let’s now do it in an iterative way and just add the value to the former sum to calculate a performant moving average in C++:

#include <iostream>
#include <vector>

class MovingAverage {
public:
    explicit MovingAverage(int size)
        : buffer(size, 0.0), maxSize(size) {}

    void addPrice(double price) {
        sum -= buffer[index];        // Subtract the value being overwritten
        sum += price;                // Add the new value
        buffer[index] = price;       // Overwrite the old value
        index = (index + 1) % maxSize;

        if (count < maxSize) count++;
    }

    double getAverage() const {
        return count == 0 ? 0.0 : sum / count;
    }

private:
    std::vector<double> buffer;
    int maxSize;
    int index = 0;
    int count = 0;
    double sum = 0.0;
};

int main() {
    // Example: 3-day moving average for Microsoft stock prices
    MovingAverage ma(3);
    std::vector<double> msftPrices = {400.0, 402.5, 405.0, 410.0, 412.0, 415.5};

    for (size_t i = 0; i < msftPrices.size(); ++i) {
        ma.addPrice(msftPrices[i]);
        std::cout << "Day " << i + 1
                  << " - Price: " << msftPrices[i]
                  << ", 3-day MA: " << ma.getAverage()
                  << std::endl;
    }

    return 0;
}

This time, no re-calculation, each time a new price gets in, we add it and average it on the fly. Although it looks better, the vector data structure in that case is not ideal.

Another approach is to use a double-entry queue: std::deque.

It perfectly suits the sliding window use case as we can pop and add elements both sides of the data structure in a very easy way (push_back/pop_front):

#include <iostream>
#include <deque>

/**
 * MovingAverage maintains a fixed-size sliding window of the most recent prices
 * and efficiently computes their average.
 */
class MovingAverage {
public:
    explicit MovingAverage(int windowSize) : size_(windowSize), sum_(0.0) {}

    // Adds a new price to the window and updates the sum accordingly
    void addPrice(double price) {
        prices_.push_back(price);
        sum_ += price;

        // Remove the oldest price if the window is too big
        if (prices_.size() > size_) {
            sum_ -= prices_.front();
            prices_.pop_front();
        }
    }

    // Returns the current moving average
    double getMovingAverage() const {
        if (prices_.empty()) return 0.0;
        return sum_ / prices_.size();
    }

private:
    std::deque<double> prices_;
    double sum_;
    int size_;
};

int main() {
    MovingAverage ma(3); // 3-day moving average

    // Historical Microsoft stock prices
    std::vector<double> msftPrices = {400.0, 402.5, 405.0, 410.0, 412.0, 415.5};

    std::cout << "Microsoft 3-day Moving Average Calculation:\n" << std::endl;

    for (size_t i = 0; i < msftPrices.size(); ++i) {
        ma.addPrice(msftPrices[i]);
        std::cout << "Day " << i + 1 << " - Price: " << msftPrices[i]
                  << ", 3-day MA: " << ma.getMovingAverage() << std::endl;
    }

    return 0;
}


It only adds/pops elements, then updates the sum, then the average.

In other terms: the time complexity O(1).

Let’s run it:

➜  build ./movingaverage
Day 1 - Price: 400, 3-day MA: 400
Day 2 - Price: 402.5, 3-day MA: 401.25
Day 3 - Price: 405, 3-day MA: 402.5
Day 4 - Price: 410, 3-day MA: 405.833
Day 5 - Price: 412, 3-day MA: 409
Day 6 - Price: 415.5, 3-day MA: 412.5

The same results but way faster!

And you pass your interview to calculate the moving average in C++ in the most performant way.

4. Common STL Container Member Functions & Tips (with Vector vs Deque)

✅ Applies to Both std::vector<T> and std::deque<T>

SyntaxDescriptionNotes / Gotchas
container.begin()Iterator to first elementUsed in range-based loops and STL algorithms ([begin, end))
container.end()Iterator past the last elementNon-inclusive range end
container.size()Number of elementsType is size_t — beware of unsigned vs signed int bugs
container.empty()true if container is emptySafer than size() == 0
container.front()First element⚠️ Undefined if container is empty
container.back()Last element⚠️ Undefined if container is empty
container.push_back(x)Add x to the endO(1) amortized for vector, always O(1) for deque
container.pop_back()Remove last element⚠️ Undefined if empty
std::accumulate(begin, end, init)Sum or fold values over rangeFrom <numeric> — works on both vector and deque

🟦 std::deque<T> Specific

SyntaxDescriptionNotes / Gotchas
container.push_front(x)Add x to frontO(1); ideal for queues and sliding windows
container.pop_front()Remove first elementO(1); ⚠️ Undefined if empty
container.erase(it)Remove element at iteratorO(1) when removing from front/back
✅ Random access ([i])SupportedSlightly slower than vector (more overhead under the hood)
April 26, 2025 0 comments

@2025 - All Right Reserved.


Back To Top
  • Home
  • News
  • Contact
  • About