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++

When an object's life begins and ends

Lesson · 10 min

The question

This function returns a reference to the best price level. It compiles without a warning and works in the unit test. Why does it crash in production?

The function

const Level& best_bid(const Instrument& ins) {
    Book book = load_book(ins);     // a local copy of the book
    return book.bids.front();       // reference into a local
}

The reference points into book, which is destroyed when the function returns. The caller receives a reference to memory that no longer holds a Level. In a unit test the stack frame may not have been reused yet, so reading through the reference happens to return the old values. In production something else is called in between, the stack is overwritten, and the read returns garbage or crashes. Undefined behaviour that works in tests is the most expensive kind.

Most C++ memory bugs in interviews are of this shape: something holds a reference, pointer, iterator or view to an object whose lifetime has ended. To find them you need precise rules for when lifetimes end.

The rules you need

Automatic objects (locals) are constructed when execution reaches their definition and destroyed at the end of the enclosing block, in reverse order of construction. That order is guaranteed and code relies on it: a lock taken first is released last.

Members are constructed in the order they are declared in the class, not the order they appear in the constructor's initialiser list, and destroyed in reverse. A member initialised from another member that is declared after it reads an unconstructed object; compilers warn about the mismatched order with -Wreorder.

Dynamic objects live from new (or the function that calls it for you) to delete. Smart pointers tie that span to the life of an owner, which is the subject of the next two lessons.

Temporaries live until the end of the full expression that created them, which is roughly the end of the statement. This is the rule that catches people:

booklocktemporarylevel&Book book; (whole function)std::lock_guard g{m};tmpokdanglingend of full expressionscope ends: unlock
Lifetimes in one function. Locals live to the end of their scope and die in reverse order; a temporary dies at the end of the full expression that created it, and a reference bound to a member of that temporary outlives the object it refers to.
const std::string& name = get_instrument().name;   // fine: member of a temporary, bound directly: extended
const Level& lvl = get_book().bids.front();         // dangling: front() returns a reference,
                                                    // the temporary Book dies at the semicolon

Lifetime extension, and where it stops

Binding a temporary directly to a const& or && local extends the temporary's life to that of the reference. That is why const std::string& s = make_string(); is safe.

The extension applies only when the reference binds to the temporary itself, or to a subobject reached by member access. It does not pass through a function call. get_book().bids.front() calls front(), which returns a reference the compiler cannot trace back to the temporary, so nothing is extended and the Book dies at the semicolon. The same trap appears in range-for loops: for (auto& l : get_book().bids) is fine, because the range expression is bound to a reference, but before C++23 for (auto& l : get_book().bids_view()) dangles if bids_view() returns a view into the temporary. C++23 extended the lifetime of temporaries in the range-for initialiser; older compilers and codebases still have the bug.

Spotting it in code review

Ask, for every reference, pointer, iterator or view: who owns the object it refers to, and is that owner guaranteed to outlive it? Patterns to flag on sight:

  • Returning a reference or pointer to a local, or to a member of a local.
  • Storing a reference or view in a member, where the referent is a function argument.
  • Taking .front(), .data(), .c_str(), begin() or a view of a temporary.
  • Capturing locals by reference in a lambda that runs later (lesson 6 of this module).
  • Keeping pointers into a container that can reallocate (lesson 7).

The tools that catch these at run time are AddressSanitizer (-fsanitize=address), which reports use-after-free and use-after-scope with the line that freed the memory, and recent compilers' dangling warnings such as -Wdangling-reference in GCC 13. They are worth naming in an interview: they are how a team stops these bugs reaching production.

The fix

Return the level by value. A Level is a price and a quantity, two machine words, cheaper to copy than to reason about. If the caller genuinely needs to refer to the live level in a long-lived book, then the book must be owned by something that outlives the reference, and the function should take the book by reference rather than build a local copy.

Check yourself

Question 1 of 3

In what order are the members of struct S { A a; B b; S() : b(), a() {} }; constructed?

Question 2 of 3

Which line leaves r dangling? Assume make_book() returns a Book by value and top() returns const Level&.

Question 3 of 3

A function returns a reference to a local and the unit test passes. Why?

In the interview

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

When is a temporary destroyed?

At the end of the full expression that created it, normally the end of the statement. The exception is when it is bound directly to a const lvalue reference or an rvalue reference, which extends it to the reference's lifetime. That extension does not pass through function calls, so a reference returned by a method of a temporary dangles.

How do you find dangling references in a large codebase?

Review for the patterns: references or views stored beyond the call, references into temporaries, pointers into containers that can grow. Then rely on tools: AddressSanitizer in the test suite catches use-after-free and use-after-scope with stack traces, and modern compilers warn on some dangling cases directly.

In what order are members destroyed, and why does it matter?

In reverse declaration order. It matters when one member uses another in its destructor, for example a worker thread that reads a queue: the queue must be declared before the thread so the thread is stopped and joined before the queue is destroyed.