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 2: Safe C++

unique_ptr, shared_ptr and weak_ptr

Lesson · 11 min

The question

Our order manager stores every live order in a std::shared_ptr and hands copies to the risk checker and the gateway. What does that cost, and would you do it differently?

Three kinds of ownership

C++ gives you three standard ways to point at a heap object, and the choice says who is responsible for deleting it.

std::unique_ptr<T>: one owner. It holds a pointer and deletes the object in its destructor. It cannot be copied, only moved, so there is always exactly one owner. It is the size of a raw pointer (with the default deleter) and costs nothing at run time beyond the delete. This should be the default.

std::shared_ptr<T>: several owners. The object is deleted when the last shared_ptr to it is destroyed. To know when that is, it keeps a reference count in a separately allocated control block.

std::weak_ptr<T>: an observer. It refers to an object managed by shared_ptr without keeping it alive. To use the object you call lock(), which returns a shared_ptr if the object still exists and an empty one if it does not.

Raw pointers and references still have a role: a non-owning parameter that the caller guarantees is alive for the call. Passing const Order& to a function that only reads the order is better than passing any smart pointer.

What shared_ptr costs

shared_ptr ashared_ptr bweak_ptr wcontrol blockstrong 2 · weak 1Order
Shared ownership in memory. The counts live in a separate control block; every copy of a shared_ptr increments the strong count atomically. With make_shared the block and the object share one allocation.
  • Size. A shared_ptr is two pointers: one to the object, one to the control block.
  • Allocation. The control block is allocated. std::make_shared<T>() allocates the block and the object together in one allocation; std::shared_ptr<T>(new T) makes two.
  • Atomic counting. Every copy increments the strong count and every destruction decrements it. These are atomic read-modify-write operations, because copies may live on different threads. Uncontended, an atomic increment costs a few nanoseconds; contended, when threads on different cores copy the same pointer, the cache line holding the count moves between cores on every operation (Module 5).
  • Unpredictable destruction. The object is destroyed on whichever thread drops the last reference, possibly in the middle of a hot path that happened to hold the last copy.

In the order manager above, each hand-off to the risk checker and the gateway copies the pointer: two atomic increments and two decrements per order, on counts that several threads touch. Each order also costs an allocation. Neither is necessary.

A better design

Ownership in an order manager is not shared. The manager owns the orders; the risk checker and the gateway look at an order while they process it. That is a single owner with borrowers:

class OrderManager {
    std::vector<Order> orders_;                 // or a pool, see Module 3
public:
    Order& create(const NewOrder& n);           // owner keeps the object
    void route(Order& o, RiskCheck& rc, Gateway& gw) {
        if (rc.check(o)) gw.send(o);            // borrowers take references, no counting
    }
};

If an order must be referred to after the call returns, for example by an exchange acknowledgement that arrives later, use an id or a handle that the manager can look up and validate, rather than a pointer that keeps the object alive (lesson 7).

Where shared_ptr is the right answer

Genuinely shared, read-mostly data with an unpredictable last user is the classic case: a configuration or a reference-data snapshot that many threads read, replaced occasionally by a new version. Readers hold a shared_ptr to the version they started with; the old version is freed when the last reader finishes. Even then, the readers should take one copy per batch, not one per message.

Cycles and weak_ptr

Two objects that hold shared_ptrs to each other keep each other alive for ever: neither count can reach zero. A session that owns its strategies, where each strategy holds a shared_ptr back to the session, leaks both. The back-reference should be a weak_ptr (or a plain reference, if the owner provably outlives the strategy):

struct Session;
struct Strategy {
    std::weak_ptr<Session> session;                 // does not keep the session alive
    void on_fill() {
        if (auto s = session.lock()) s->record_fill();  // nullptr if the session is gone
    }
};

A detail worth knowing: with make_shared, the object and control block are one allocation, so the memory is not returned until the last weak_ptr is gone as well, even though the object's destructor ran earlier. For large objects observed by long-lived weak pointers, that can matter.

Check yourself

Question 1 of 3

What is sizeof(std::unique_ptr<Order>) with the default deleter on a 64-bit platform?

Question 2 of 3 · choose every correct option

Which operations on std::shared_ptr perform an atomic read-modify-write? Choose all that apply.

Question 3 of 3

A parent object holds std::shared_ptr<Child> and each child holds std::shared_ptr<Parent>. What happens when the last outside reference to the parent is dropped?

In the interview

Say your own answer out loud first, then open the one we would give.

When would you use shared_ptr versus unique_ptr?

unique_ptr by default: one owner, no overhead beyond the pointer. shared_ptr only when ownership is genuinely shared and I cannot say in advance who finishes last, such as a read-mostly snapshot handed to several threads. Functions that only use an object should take a reference, not a smart pointer.

What does shared_ptr cost?

Two pointers in size, a control block allocation, which make_shared merges with the object's, and an atomic increment or decrement on every copy and destruction. Under contention those atomics bounce a cache line between cores. And the object is destroyed on whatever thread drops the last reference, which can land in a hot path.

What is weak_ptr for?

Observing an object managed by shared_ptr without keeping it alive, typically to break ownership cycles or for caches. You call lock() to get a temporary shared_ptr, which is empty if the object has gone.

Why prefer make_shared?

One allocation for the object and the control block instead of two, better locality, and no window where a raw new has happened but ownership has not been taken. The trade-off is that the memory is not released until the weak references are gone too.