Quant Developer Interview Prep
Quant firms increasingly don't ask LeetCode. They hand you a small systems problem — own a resource correctly, keep a structure sane under contention, publish state without locking — problems with no trick, that expose whether you actually understand ownership, memory, and concurrency. This track drills that shape, plus the daily mental-math and probability reps that gate the early rounds.
Quant Developer Interview Prep progress: 0/26 complete (0%)
- 01
Explain mode — the C++ follow-ups
Every milestone in this track is decided by a follow-up question, not by the code. Someone can finish the bounded blocking queue and still lose the round on "what does pop() hand back, and who owns it now?" These are the C++ questions that actually get asked — write the answer out, in your own words, before you look anything up.
- What is the difference between a thread and a process?The opener before any concurrency question. Answer in terms of the address space.
- What is a mutex?The canonical opener. Most wrong answers contain every right keyword.
- Lvalue references versus rvalue references — what binds to what?The grammar underneath move semantics. Get this wrong and std::move never makes sense.
- What does std::move actually do?It moves nothing. Say what it does instead.
- Why does a polymorphic base need a virtual destructor?Name the exact construct that is undefined behaviour.
- What is RAII, and what exactly does it guarantee?The guarantee is about stack unwinding, not about tidiness.
- unique_ptr or shared_ptr — how do you choose?An ownership question wearing a smart-pointer costume.
- What is thread-safe about std::shared_ptr?The count is. The object is not. The pointer itself is not either.
- Why must a move constructor be noexcept?The answer is std::vector's strong exception guarantee.
- Acquire/release versus seq_cst — what is the difference?The M6 question. Answer in terms of what may be reordered.
- What does volatile do in C++, and what does it not do?It is not a threading tool. Say what it is for.
- Why is `return std::move(x)` usually wrong?Because it defeats the optimisation it looks like it is helping.
- What is undefined behaviour, and why do these firms care so much?Say what the compiler is entitled to assume, not just what might happen.
- 02
Daily drills
Runs in parallel with everything else, every day, from day one. Optiver and IMC gate on a timed arithmetic test before a human ever sees you — this is not a phase you finish.
- 03
Foundations — ownership and lifetime
The exercises that train what the opening round is really probing: who owns this, when does it die, and what happens if something throws halfway through. Small enough to finish in an evening, deep enough to expose everything you don't know.
- Implement unique_ptrThe canonical opener. Rule of five, custom deleter, no copies.
- Wrap an OS handle in RAIIA move-only handle around a file descriptor. Rule of five, no copies.
- Intrusive reference countingShared ownership with the count inside the object. Atomics and cycles.
- A vector with inline storageSmall-buffer optimisation, placement new, strong exception guarantee.
- 04
Depth — hashing and concurrency
Where interviews stop being about syntax and start being about judgement under contention.
- 05
Low latency
The part that separates a good C++ developer from someone a trading firm wants. Measurement is the skill, not opinion.
- 06
Capstone
The artifact you actually talk about in interviews, plus the round everyone underprepares.
- 07
Interview loop
Runs weekly from the Depth module onward. Practice under pressure, not in comfort.
Cheatsheet
Every explanation in this track, answered. Use it to revise, not to prepare — an answer you have read is not the same as one you can give under a follow-up, and the difference is what the interview measures.
What is the difference between a thread and a process?
A process owns an address space. The threads inside it share that one address space, so they see the same heap, the same globals and the same open file descriptors, while each thread gets its own stack and its own register state. That sharing is why threads are cheap: creating one does not build a new address space, and switching between two threads of the same process does not swap page tables. It is also the whole risk — because there is no isolation, one thread writing through a bad pointer corrupts memory the others are using, and an unhandled fault takes the entire process down rather than just that thread. Processes pay for creation, switching and IPC, and get containment in return.
Follow-up · You said threads share memory. Which parts are not shared — and what happens if one thread hands another a pointer into that?
Answer it yourself →What is a mutex?
A mutex is a lock that lets only one thread at a time run a section of code. Any other thread that tries to take it waits until the holder releases it. It does not protect a variable — nothing in the language connects a mutex to any particular data, so it only works if every access to that data goes through the same mutex, and keeping that discipline is on you. Because releasing is mandatory and an exception can leave a scope early, you take it with a lock_guard rather than calling unlock by hand.
Follow-up · You said a mutex locks the data. What happens if two threads lock two different mutexes in opposite order?
Answer it yourself →Lvalue references versus rvalue references — what binds to what?
An lvalue is an expression naming an object with an identity — you can take its address. An rvalue is a value without one, typically a temporary about to expire. A plain T& binds only to lvalues, a const T& binds to both, which is why a const reference can accept a temporary and extend its life. A T&& binds only to rvalues, and that is what lets you write an overload that knows its argument is about to die and can safely steal from it. The part that catches people is that once you are inside the function, the parameter has a name, and anything with a name is an lvalue — so passing it on selects the copy unless you std::move it again. And T&& on a deduced template parameter is not an rvalue reference at all; it is a forwarding reference that can deduce as either, which is what std::forward exists to preserve.
Follow-up · Inside void f(T&& x), you call g(x). Which overload of g does that pick, and why does it surprise people?
Answer it yourself →What does std::move actually do?
std::move does not move anything. It is a cast that turns its argument into an rvalue reference, and it compiles to no instructions at all. What it changes is overload resolution: the move constructor or move assignment gets chosen instead of the copy, and those do the actual work. Afterwards the source is in a valid but unspecified state — you may destroy it or assign it a new value, but you should not assume it is empty. On a const object the cast produces a const rvalue, which no move constructor accepts, so you silently get a copy.
Follow-up · You said the source is left empty. Is that guaranteed by the standard for every type, or only for the standard library containers?
Answer it yourself →Why does a polymorphic base need a virtual destructor?
Deleting a derived object through a base pointer whose destructor is not virtual is undefined behaviour — not merely a leak. With a virtual destructor the call dispatches through the vtable, so the derived destructor runs and then the base's, in order. It is not free: it gives the type a vptr, so you do not put one on every class by reflex. For a base you never delete polymorphically the alternative is a protected non-virtual destructor, which makes deleting through the base impossible to write in the first place.
Follow-up · std::shared_ptr<Base> holding a Derived calls the right destructor even without a virtual one. Why — and does that let you drop it?
Answer it yourself →What is RAII, and what exactly does it guarantee?
The constructor acquires a resource and the destructor releases it. The guarantee is the part that matters: the compiler emits the destructor call whenever the scope exits, including when it exits because an exception is propagating, so no path skips the release. That is why it is not only about memory — locks, file descriptors, sockets and transactions all want the same shape. It does not save you from everything: an object you never destroy, a call to std::exit, or a destructor that throws while an exception is already unwinding are all still yours to get right.
Follow-up · Your destructor releases the resource. What happens if it throws while an exception is already propagating, and what does that mean for how you write it?
Answer it yourself →unique_ptr or shared_ptr — how do you choose?
They express different things, so the question is really who owns the object. unique_ptr means exactly one owner, it is move-only, and it compiles down to a raw pointer with a destructor — there is no runtime cost to paying for it. shared_ptr means the lifetime is genuinely shared and nobody can say in advance who goes last, so it keeps a reference count in a control block next to the object. That count is atomic, so every copy is a synchronised increment and every destruction a decrement; in a hot path you pass by const reference rather than by value to avoid paying it. I default to unique_ptr and treat reaching for shared_ptr as a signal to check whether the ownership is actually shared or just unclear. And shared ownership can form cycles that never reach zero, which is what weak_ptr is for.
Follow-up · You picked shared_ptr because two objects both need the pointee. How would you tell that apart from simply not knowing who owns it?
Answer it yourself →What is thread-safe about std::shared_ptr?
Three separate things, and the question is really asking whether you separate them. The reference count in the control block is atomic, so copying and destroying shared_ptrs that point at the same object from different threads is safe. The managed object is not protected at all — if two threads call a mutating method on it, you need your own mutex. And a shared_ptr variable is itself just an object: two threads writing the same instance is a data race, which needs std::atomic<std::shared_ptr> or a lock. The atomics also cost, since every copy is a potentially contended increment — which is why you pass by const reference rather than by value in hot code.
Follow-up · Two threads each hold their own shared_ptr to the same object and both call a non-const method. Which of your three parts covers that?
Answer it yourself →Why must a move constructor be noexcept?
Because std::vector owes callers the strong exception guarantee on reallocation: if anything throws while it is moving elements into the new buffer, the vector must be left exactly as it was. A move that throws cannot be undone halfway, since the elements already moved have been gutted. So the library calls std::move_if_noexcept, which falls back to copying when your move constructor is not marked noexcept. Nothing breaks — you simply pay for copies while believing you have move semantics, and correctness is unaffected, which is exactly why it is easy to miss.
Follow-up · Your move constructor allocates. Can you still mark it noexcept, and what are your options if you want vector to move rather than copy?
Answer it yourself →Acquire/release versus seq_cst — what is the difference?
A release store and an acquire load on the same atomic pair up: when the consumer's acquire load observes the value the producer's release store wrote, everything the producer did before that store is visible to the consumer afterwards. That is the happens-before edge a producer/consumer handoff needs. seq_cst gives you that as well, plus a single total order that all seq_cst operations agree on — which acquire/release does not provide, and which you need only when several atomics have to be ordered against one another. The cost is architectural: on x86 acquire loads and release stores are ordinary instructions and only seq_cst stores need a fence, so the gap is small; on ARM the weaker orderings genuinely save barriers.
Follow-up · Your ring buffer uses relaxed on the load of the other index. What exactly are you giving up, and why is it still correct here?
Answer it yourself →What does volatile do in C++, and what does it not do?
volatile tells the compiler that reads and writes of that object are observable side effects: it may not cache the value in a register, elide an access it believes is redundant, or reorder it against other volatile accesses. That is what memory-mapped hardware registers and signal-handler variables need. It gives you nothing across threads — no atomicity, so a volatile increment is still a read-modify-write and still races, and no fences, so it establishes no happens-before with any other thread. The Java meaning is different; in C++ the tool for inter-thread work is std::atomic.
Follow-up · If volatile does not help across threads, why does a spin loop on a plain non-volatile bool sometimes hang forever, and what is the right fix?
Answer it yourself →Why is `return std::move(x)` usually wrong?
Because it defeats the optimisation. Returning a local by value is already elided wherever the compiler can manage it — the object is constructed directly in the caller's storage, so there is no copy and no move. Writing return std::move(x) makes the return expression an rvalue reference rather than the name of a local, which disqualifies NRVO and forces a real move. Even where elision does not apply, the implicit-move rule already treats a returned local as an rvalue, so std::move buys nothing. It is correct in the cases where elision was never available: returning a member, or a by-value parameter.
Follow-up · You return one of two named locals depending on a branch. Does NRVO still apply, and does your answer change?
Answer it yourself →What is undefined behaviour, and why do these firms care so much?
Undefined behaviour means the standard imposes no requirements at all — on the whole program, not just on the offending line. The practical consequence is that the optimiser may assume it never occurs and transform code on that assumption: dereference a pointer and then test it for null, and the test can be deleted, because the dereference would have been UB had it been null. Common sources are signed overflow, dereferencing null, reading an uninitialised value, out-of-bounds access, data races and modifying a string literal. It differs from unspecified behaviour, where the implementation picks among valid options and need not document the choice, and from implementation-defined behaviour, where it picks and must document it. These firms care because release builds optimise hard, so UB surfaces as code that works at -O0 and quietly changes at -O2.
Follow-up · A signed int overflows in a loop counter and the loop never terminates at -O2 but does at -O0. Walk me through what the optimiser did.
Answer it yourself →