5.2.22 · D5C++ Programming
Question bank — Lambda expressions — capture list (by value, by reference)
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):
- When is the copy/alias formed? Always at construction (the
[...]line), never at invocation — this is the first true/false trap. - 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. - Do I want to change the original? Yes → reference (the
++countexample). Only my private copy →mutablevalue (the[n]() mutable { n++; }example, which never touches the outsiden).