Quant developer HFT course · Module 2: Safe C++
Lambdas and what they capture
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 capturesthisimplicitly 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::functionallocate (Module 1). - A lambda capturing a
shared_ptrby 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?
Capture by reference stores a reference, typically as a pointer. It is valid only while x is.
Question 2 of 3
Which lambda is safe to post to a thread pool from inside a member function that returns immediately?
The init capture moves the data into the lambda, so the lambda owns it. The first and last still use this implicitly to reach members; the second refers to a local that will be gone.
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?
[=] implicitly captures this, and members are accessed through it. It looks like capture by value but is a pointer to the object. C++20 deprecates the implicit capture; [*this] copies the object.
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.