Exercises — Lambda expressions — capture list (by value, by reference)
This page revisits the parent topic Lambda expressions — capture list (by value, by reference) and leans on prerequisites References vs pointers in C++, Object lifetime and scope, Function objects (functors), std::function, const member functions, and std::thread and async.
Level 1 — Recognition
Goal: read a capture list and say WHAT it means, no execution needed.
Recall Solution 1.1
[a]→aby value.[&a]→aby reference.[=]→ all used (a,b,c) by value.[&]→ all used (a,b,c) by reference.[=, &b]→ default by value (a,ccopies), butbby reference.[&, a]→ default by reference (b,caliases), butaby value.
Why: the leading = or & sets the default; each explicit entry after it is the exception.
Recall Solution 1.2
puses[n]→ photo (frozen snapshot ofn = 5).quses[&n]→ phone line (live wire to the realn).
Level 2 — Application
Goal: execute a single lambda in your head and produce the printed number.
Recall Solution 2.1 — prints
1
The copy happened at the [n] line while n == 1. Rewritten as a struct, the member int n was initialised to 1. Later n = 99 edits only the outside variable. Answer: 1.
Recall Solution 2.2 — prints
99
The member is int& n (an alias to the real n). At call time it dereferences the current value, which is 99. Answer: 99.
Recall Solution 2.3 — A=
11, B=12, C=10
mutable drops the const on operator(), so the internal copy member (start 10) can change. Call 1: 10→11, returns 11. Call 2: 11→12, returns 12 — the copy persists between calls because it lives inside the lambda object. The original n is a separate object, still 10. A=11, B=12, C=10.
Level 3 — Analysis
Goal: reason across two captures, mixed defaults, and side effects.
Recall Solution 3.1 — r=
23, a=1, b=12
[=, &b]: a is a copy member, b is a reference member.
Inside m(): copy-a goes 1→11; real-b goes 2→12. Returns 11 + 12 = 23.
Outside: original a untouched (1), original b edited via the reference (12).
r=23, a=1, b=12.
Recall Solution 3.2 — prints
5 20
byVal froze 5 at creation. byRef aliases the live x, now 20. Output: 5 20.
Recall Solution 3.3 — prints
2
Both lambdas alias the same count. Three ++ then one --: 0 → 3 → 2. Both are phone lines to the same person. Answer: 2.
Level 4 — Synthesis
Goal: combine capture rules with lifetime and closure identity.
Recall Solution 4.1 —
Undefined behaviour (dangling reference)
x is a local of make(). When make() returns, x's storage is destroyed (see Object lifetime and scope). The returned closure still holds &x — a dangling reference. Calling bad() reads dead memory → UB (may print 42, garbage, or crash).
Fix: capture by value so the value lives inside the returned closure. Change [&x] → [x] (delete the &, a one-character fix). Then the closure carries its own copy 42 and is safe to return.
Recall Solution 4.2 — by value prints
012; by reference is UB
By value [i]: each iteration copies the current i (0, then 1, then 2) into that iteration's closure. Output 0 1 2 → printed 012.
By reference [&i]: every closure aliases the same loop variable i. But i is scoped to the for; after the loop it is out of scope → all three references dangle → UB. (Even before it dies, all three would read the same final value, not 0/1/2.)
This is the classic reason to prefer [i] (value) when storing closures.
Recall Solution 4.3 — prints
100; hazard: dangling this
[this] stores the pointer to w (a phone line to the whole object). id is read through that pointer, so at call time it sees the current w.id == 100. Prints 100.
Hazard: the closure keeps w alive nowhere — it only borrows this. If w is destroyed before g() runs (e.g. w was a temporary or went out of scope), g() dereferences a dead object → UB. (C++17+ [*this] copies the whole object to avoid this.)
Level 5 — Mastery
Goal: full trace of a program that mixes value, reference, mutable, and shared state.
Recall Solution 5.1 — r1=
101, r2=103, r3=106, hits=3
Members: copy base (starts 100, persists via mutable), reference hits (aliases the real hits).
- Call 1:
hits0→1; copy-base100→101; returns 101. - Call 2:
hits1→2; copy-base101→103; returns 103. - Line Z:
base = 0edits the originalbase— but the lambda holds a separate copy, so itsbaseis still103. No effect on the closure. - Call 3:
hits2→3; copy-base103→106; returns 106. - Final
hits: mutated by reference three times from 0 → 3. r1=101, r2=103, r3=106, hits=3.
Recall Solution 5.2
struct __Closure {
int base; // BY VALUE -> its own copy
int& hits; // BY REFERENCE -> alias to outside hits
int operator()() { // 'mutable' -> NOT const, so 'base' is writable
++hits;
base += hits;
return base;
}
};
__Closure make{ base, hits }; // base copied, hits aliasedWhy line Z didn't matter: the member int base is a distinct object initialised once at construction; assigning the outside base never touches this separate member — see Function objects (functors) for the struct-with-operator() view.
Wrap-up figures
The value-vs-reference divide is one picture: a copy sitting inside the closure box versus an arrow pointing out to the original.

And the dangling trap (Exercises 4.1 / 4.2) is a timeline: the reference outlives what it points to.

Recall One-line summary
Rewrite every lambda as a struct: [x] → copy member (frozen, needs mutable to edit, safe to escape); [&x] → reference member (live, shared, dangles if it escapes its variable's lifetime).