5.2.22 · D5C++ Programming

Question bank — Lambda expressions — capture list (by value, by reference)

2,506 words11 min readBack to topic

True or false — justify

A by-value capture [x] makes a copy at the moment the lambda is invoked (called).
False — the copy happens at the moment the closure object is constructed (the [x] line runs), not when you later invoke it with (). Construction (making the closure) and invocation (running operator()) are two different events; the snapshot is taken at construction, so later changes to the original are invisible.
[&x] copies the address of x once, so it's a cheap fixed cost regardless of x's size.
True — a reference capture stores an alias (address-sized), so a huge object costs the same to capture by reference as a tiny one; only the copy path scales with size.
Two calls to the same by-value mutable lambda start from the same fresh copy each time.
False — the copy is a member of the lambda that persists between calls; [n]() mutable { n++; return n; } returns 11 then 12, because the internal n carries over.
Adding mutable lets a lambda change the original captured variable.
False — mutable only removes the const on operator(), allowing edits to the lambda's own copy; the original outside variable is a separate object and stays untouched.
[=] and [&] both capture every local variable declared in the enclosing function.
False — both capture only the variables the body actually odr-uses (needs at runtime); variables you never mention cost nothing and aren't stored.
Capturing by reference is always more efficient than by value.
False — the trade-off cuts both ways. A reference stores just an address, but every access costs a pointer indirection (a hop through memory to reach the real data). A by-value copy of a small POD (like an int) is essentially free to make and then lives in-register/inline with no indirection, so it can actually be faster to use than a reference — reference-capture only wins clearly when the object is large and copying it would be expensive.
A lambda with an empty capture list [] can still be converted to a plain function pointer.
True — with no captures there's no state to carry, so the closure is equivalent to a free function and is convertible to a function pointer; any capture kills that conversion.
Two lambdas written with identical text and identical captures have the same type.
False — each lambda expression produces a unique, unnamed closure type, so even textually identical lambdas are distinct types (this is why you store them in auto or std::function).
[x = expr] (C++14 init-capture) requires that x already exist as a variable in the enclosing scope.
False — init-capture creates a brand-new closure member x initialized from expr; there need be no outside variable named x at all, which is exactly how you introduce fresh state or capture a moved value.
[*this] (C++17) and [this] store the same thing in the closure.
False — [this] stores the pointer (an alias to the live object, danglable), while [*this] copies the whole *this object into the closure, so the lambda owns an independent snapshot that survives the original's death.

Spot the error

auto f = [&x]() { return x; }; returned from a function where x is a local — what's wrong?
This is the "arrow out of the box" case from the figure: x dies when the function returns, so the stored reference dangles; calling f later reads dead stack memory (UB). Capture [x] by value so the copy lives inside the closure.
int n=10; auto c=[n](){ n++; return n; }; — why won't this compile?
operator() is const by default, so the by-value member n is read-only; n++ is rejected. Add mutable to allow editing the internal copy.
[=, x] — what's illegal about this capture list?
You can't repeat a capture that the default already covers with the same mode; [=] already takes x by value, so naming x by value again is redundant/ill-formed. Only the opposite mode as an exception ([=, &x]) is allowed.
[&, &x] — why is this rejected?
The default & already captures everything by reference, so listing x by reference again duplicates it; an exception must differ from the default (e.g. [&, x]).
auto lam = [this]() { return value; }; stored in a container that outlives the object — the trap?
[this] captures the raw this pointer (an "arrow out"); if the enclosing object is destroyed first, this dangles just like a reference. See Object lifetime and scope — prefer [*this] (C++17, copies the object in) or capture the needed members by value if the lambda escapes.
auto f = [p]() { return *p; }; where p is a std::unique_ptr<int> — why won't it compile?
unique_ptr is move-only (no copy constructor), and [p] asks for a by-value copy; the compiler rejects it. Use init-capture [p = std::move(p)] to move ownership into the closure instead.
Passing [&]-capturing lambda to a thread via std::thread and async, then letting the caller scope end — the bug?
The thread may run after the local variables die, so the references dangle across threads. Capture by value (or move a unique_ptr in with [p = std::move(p)]) so the worker owns its data independently of the caller's stack.

Why questions

Why does the parent note say a lambda is "secretly a struct"?
Because the compiler literally rewrites it into an anonymous class with operator(); captures become member variables. This single mental model explains value vs reference, const, and mutable all at once — it's the same as a hand-written functor.
Why is operator() const by default, and how does that connect to by-value captures?
A const call operator promises not to mutate the closure's state, mirroring const member functions; that makes by-value members read-only, which is why editing one requires mutable.
Why does by reference "see the latest value" while by value "sees a frozen snapshot"?
A reference member is an alias to the original memory, so every read dereferences the same location the original lives in; a value member is an independent copy taken once at creation, disconnected from later writes.
Why prefer capturing by value when a lambda is stored, returned, or handed to async code?
Because the lambda's lifetime then exceeds the enclosing scope; by value the data lives inside the closure, so nothing can dangle. This is the "carry the sandwich, don't leave a note" rule.
Why can a by-reference capture be preferred even though by value is safer?
When you genuinely want to mutate the outside world (e.g. a running counter ++count) or the captured object is huge and the lambda clearly stays within its scope, a reference is both correct and cheap.
Why is [&x] cheaper to store than [bigObject]?
A reference is address-sized regardless of what it points to (see References vs pointers in C++), whereas a by-value capture must copy-construct the entire object into the closure, which scales with its size.
Why does capturing [this] behave more like by-reference than by-value?
It stores the pointer to the current object, not a copy; the lambda then reaches through it to live members, so if the object dies the pointer dangles — the reference-style lifetime hazard. [*this] fixes this by copying the object in.
Why was init-capture [p = std::move(p)] added to the language?
Pre-C++14 captures could only copy or reference an existing variable, so move-only types like std::unique_ptr could not enter a closure at all; init-capture lets you move-construct a member, transferring ownership into the lambda.

Edge cases

A lambda captures nothing and uses only its parameters — what does the capture list look like and why?
It's []; parameters are supplied at call time, not from the enclosing scope, so no capture is needed. Such lambdas are the ones convertible to function pointers.
What happens if a by-value capture is of a type with an expensive copy but you only read it?
You pay a full copy at creation for read-only use; if the lambda stays in scope, capturing by reference (or const reference intent) avoids the copy, but only when lifetime safety is guaranteed.
A global (or static) variable is used in the body but not listed in [...] — is that an error?
No — globals and statics are not local variables, so they aren't captured; the lambda simply refers to them directly. Only enclosing local variables need capturing.
[=] is used but the body reads a variable that is never modified after creation — any difference from [&]?
Observably none for the value read, but [=] copies (safe to outlive scope) while [&] aliases (dangles if it escapes); with no later mutation the only real distinction is lifetime safety and copy cost.
A mutable by-value lambda is copied into a second variable — do they share the internal counter?
No — copying the lambda copies its member state, so each copy has its own independent counter; incrementing one does not affect the other.
Capturing by value a variable of reference type — is the copy live or frozen?
The by-value capture copies the referent's value into the closure at creation, giving a frozen snapshot; the closure member is a plain value, not itself a reference, so later changes to the original aren't seen.
After [p = std::move(p)] moves a std::unique_ptr in, what is the state of the outside p?
The moved-from outside p is left empty (null) — ownership now lives inside the closure; using the old p to dereference would be a bug, and the lambda alone is responsible for the object's lifetime.
Can a lambda that captured a unique_ptr by move be copied?
No — because it holds a move-only member, the closure itself becomes move-only; you can move the lambda but not copy it (and it can't be stored in a std::function, which requires copyability).
What does [*this] cost compared with [this], and when is it worth it?
[*this] copies the entire enclosing object into the closure (memory + copy cost), whereas [this] copies just a pointer; the copy is worth it whenever the lambda escapes the object's lifetime (async work, stored callbacks) so it can't dangle.

Recall Reasoning checklist — tied to the two figures and the examples above

Walk these three questions against Figure 1 (copy-inside vs arrow-out) and Figure 2 (the decision map):

  1. When is the copy/alias formed? Always at construction (the [...] line), never at invocation — this is the first true/false trap.
  2. Does the lambda outlive the variable/object? If yes you're on the "arrow out" side of Figure 1 and the escaping branch of Figure 2 — use value, [*this], or move-in ([p = std::move(p)]), exactly as in the return-a-lambda and hand-to-thread errors.
  3. Do I want to change the original? Yes → reference (the ++count example). Only my private copy → mutable value (the [n]() mutable { n++; } example, which never touches the outside n).