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

Lambdas and what they capture

Lesson · 9 min

The question

This code registers a callback that fires when an order is acknowledged. It works for a while and then crashes. What is wrong?

The code

void OrderRouter::submit(const NewOrder& n) {
    Order order = make_order(n);
    pending_count_++;
    gateway_.send(order, [&](const Ack& ack) {      // runs later, on the gateway's thread
        order.status = ack.status;                  // order: a local of submit, long gone
        pending_count_--;                           // this: is the router still alive?
    });
}

The lambda captures everything by reference with [&]. It is stored by the gateway and runs when the acknowledgement arrives, by which time submit has returned and order has been destroyed. The write goes to stale stack memory: sometimes harmless, sometimes overwriting the frame of whatever function is running now.

What a lambda is

A lambda expression creates an object of a compiler-generated class. Each captured variable becomes a member, and the body becomes operator().

// [&order, this](const Ack& a) { ... }   is roughly
struct __Lambda {
    Order& order;          // capture by reference: a reference member
    OrderRouter* self;     // capturing this: a pointer member
    void operator()(const Ack& a) const { ... }
};

Capture by value copies the variable into the object at the point the lambda is created. Capture by reference stores a reference. Capturing this stores the pointer, not the object, so the members it uses are read through that pointer when the lambda runs.

So the rule is simple: a lambda that runs after its creating scope ends must not hold references to that scope's locals, and must not hold this unless the object outlives every invocation.

The two kinds of lambda

Immediate lambdas run before the enclosing function returns: arguments to std::sort, std::find_if, a for_each. Capturing by reference is correct and efficient.

Deferred lambdas are stored and run later: callbacks, tasks posted to a thread pool, timers, completion handlers. They need to own what they use.

Fixing the callback

void OrderRouter::submit(const NewOrder& n) {
    OrderId id = orders_.create(n);                 // the router owns the order
    pending_count_++;
    gateway_.send(orders_.get(id), [this, id](const Ack& ack) {
        if (Order* o = orders_.find(id)) o->status = ack.status;   // look up, may be gone
        pending_count_--;
    });
}

The order now lives in the router's table and the lambda carries its id by value. It still captures this, so the router must outlive the gateway's pending callbacks, which should be guaranteed by shutdown order: stop the gateway, drain or cancel its callbacks, then destroy the router. If that cannot be guaranteed, capture a std::weak_ptr to the router and lock it when the callback runs.

Note also pending_count_: if the callback runs on another thread, the counter needs to be atomic or protected (Module 4). A captured reference across threads is a lifetime question and a data race question at once.

Habits worth stating

  • Write captures explicitly in deferred lambdas. [&] and [=] hide what is held. [=] in a member function captures this implicitly in older standards, which looks like capture by value and is not (C++20 deprecates it).
  • Use init captures to move ownership in: [buf = std::move(buffer)] puts the buffer inside the lambda.
  • Keep deferred lambdas small; large captures make std::function allocate (Module 1).
  • A lambda capturing a shared_ptr by value keeps the object alive until the lambda is destroyed, which can be much later than you think if the lambda is stored.

Check yourself

Question 1 of 3

What does [&x] store in the lambda object?

Question 2 of 3

Which lambda is safe to post to a thread pool from inside a member function that returns immediately?

Question 3 of 3

In C++17 and earlier, what does [=] capture when used inside a member function and the body uses a member variable?

In the interview

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

When is capturing by reference in a lambda dangerous?

When the lambda runs after the scope that owns the captured variables has ended: callbacks, tasks on another thread, timers. The lambda holds references to destroyed locals. For lambdas used immediately, such as in algorithms, capturing by reference is fine.

What does capturing this mean?

The lambda stores the pointer, not a copy of the object. Members are read through it when the lambda runs, so the object must still be alive then. If that is not guaranteed, capture a weak_ptr and lock it, or capture the specific values needed by value.

How do you move a unique_ptr into a lambda?

With an init capture: [p = std::move(ptr)]. The lambda becomes move-only, so it cannot be stored in a std::function that requires copyability; it needs std::move_only_function or a template parameter.