Intuition What this deep dive does
The parent note taught you the two ideas: ==value = a photo, reference = a phone line==. This page hunts down every case those two ideas can produce — every capture mode, every lifetime, every "gotcha" — and works each one until there is no scenario left that can surprise you.
If a term feels new, it was earned in the parent note: a capture is a variable from the surrounding code that the lambda is allowed to see; by value stores a private copy inside the lambda; by reference stores an alias (an address) to the original.
Every question about capture lists is really a question about two axes :
How did we capture it? (copy vs alias, explicit vs default, mutable or not)
Whose lifetime is longer — the lambda or the variable it captured?
Cross those and you get the complete grid. Each cell below is worked in an example.
#
Cell
Capture
Key twist
Example
A
Snapshot
[x]
original changes after creation
Ex 1
B
Live wire
[&x]
original changes after creation
Ex 2
C
Editable copy
[x] mutable
copy persists across calls
Ex 3
D
Mixed default
[=, &b]
some copied, some aliased
Ex 4
E
Dangling (degenerate)
[&x] escapes scope
reference to dead variable
Ex 5
F
Zero / empty
[]
captures nothing — compile behaviour
Ex 6
G
Timing of the snapshot
[x] created in a loop
when the copy is taken
Ex 7
H
Real-world word problem
[&total]
accumulate a running sum
Ex 8
I
Exam twist
[this] vs [*this]
capturing the object
Ex 9
The picture below is the whole matrix at a glance — keep it open as you read.
Worked example Ex 1 — Cell A: the snapshot
[x]
int price = 100 ;
auto quote = [ price ]() { return price; };
price = 250 ;
std ::cout << quote (); // ?
Forecast: guess the printed number before reading on.
[price] copies price into the lambda's hidden member. Why this step? By value = a photo. The photo is taken at the [price] line , where price is 100.
price = 250 edits only the outside variable. Why this step? The lambda's copy is a different object in memory — the assignment cannot reach it.
quote() reads the private copy → 100.
Verify: the copy was frozen at 100, nothing touched it, so 100 is the only possible answer. ✔
Worked example Ex 2 — Cell B: the live wire
[&x]
int price = 100 ;
auto quote = [ & price ]() { return price; };
price = 250 ;
std ::cout << quote (); // ?
Forecast: same code as Ex 1 but with &. Guess the number.
[&price] stores the address of price. Why this step? By reference = a phone line. No copy is made; the lambda remembers where price lives.
price = 250 writes 250 into that exact memory.
quote() dereferences the address and reads whatever is there now → 250. Why this step? Every call re-reads the original, so it always sees the current value.
Verify: the alias and the original name the same memory; the last write wins → 250. ✔
Worked example Ex 3 — Cell C: editable copy
[x] mutable
int seed = 10 ;
auto next = [ seed ]() mutable { seed += 5 ; return seed; };
int a = next (); // ?
int b = next (); // ?
std ::cout << seed; // ?
Forecast: predict a, b, and the final seed.
[seed] copies 10 into the member. Why this step? Photo at creation time.
mutable removes the default const on operator(). Why this step? Without it the copy is read-only and seed += 5 would not compile. mutable lets you edit the copy — never the original.
First call: copy 10 + 5 = 15, returns 15. The member keeps 15 (the closure is one persistent object).
Second call: copy 15 + 5 = 20, returns 20.
seed outside is untouched → 10.
Verify: a = 15, b = 20, seed = 10. The copy climbs 10→15→20; the original never moved. ✔
Worked example Ex 4 — Cell D: mixed default
[=, &b]
int a = 1 , b = 2 ;
auto m = [ = , & b ]() mutable { a += 10 ; b += 10 ; return a + b; };
int r = m (); // ?
std ::cout << a; // ?
std ::cout << b; // ?
Forecast: predict r, then a and b after the call.
[=, &b] = "default by value, except b by reference." Why this step? a is used and no exception is listed, so it is copied; b is explicitly aliased.
Inside: copy of a becomes 1 + 10 = 11. Why this step? mutable allows editing the copy.
b += 10 writes through the alias: original b becomes 2 + 10 = 12.
Returns 11 + 12 = 23.
After the call: outside a is still 1 (only the copy changed); outside b is 12 (edited via reference).
Verify: r = 23, a = 1, b = 12. Copy edited internally, alias edited the real thing. ✔
Worked example Ex 5 — Cell E: the dangling reference (degenerate lifetime)
auto make () {
int local = 42 ;
return [ & local ]() { return local; }; // local dies at return!
}
auto bad = make ();
// bad(); // reads dead memory -> undefined behavior
Forecast: why is this the one that crashes ?
make() creates local = 42 on its stack. Why this step? It is a local variable — its lifetime ends when make() returns (see Object lifetime and scope ).
[&local] captures the address of local. The lambda now holds a phone line to a person about to leave the building.
make() returns → local is destroyed. The address inside bad now points at reclaimed stack memory.
Calling bad() dereferences a dead address → undefined behaviour. Why this step? The lambda outlived its captured variable — the fatal ordering.
Fix: capture by value — [local] — so 42 lives inside the returned closure. This is the rule from the parent: capture by value when the lambda escapes its scope (returned, stored, handed to std::thread and async ). See References vs pointers in C++ for why a dangling reference is exactly a dangling pointer in disguise.
Verify: with [local] the returned closure carries its own 42; bad() safely returns 42. ✔
Worked example Ex 6 — Cell F: the empty capture
[]
int outside = 5 ;
auto pure = []( int x ) { return x * 2 ; }; // OK
// auto bad = []() { return outside; }; // COMPILE ERROR
std ::cout << pure ( 7 ); // ?
Forecast: which line refuses to compile, and what does the good one print?
[] captures nothing. Why this step? The empty list is a promise: "I use no outside locals."
pure only touches its parameter x. Why this step? Parameters are not captures — they arrive fresh on each call — so [] is fine.
The commented line reads outside, a non-captured local → compile error. Why this step? The lambda broke its [] promise.
pure(7) → 7 * 2 = 14.
Verify: pure(7) = 14. A capture-nothing lambda is effectively a plain functor with no state, freely convertible to a std::function or a raw function pointer. ✔
Worked example Ex 7 — Cell G:
when is the copy taken? (loop timing)
std ::vector < std ::function <int () >> fns; // see std::function
for ( int i = 0 ; i < 3 ; ++ i) {
fns. push_back ([ i ]() { return i; }); // by VALUE
}
// fns[0](), fns[1](), fns[2]() -> ?
Forecast: what do the three stored lambdas return?
Each iteration i has a value: 0, then 1, then 2. Why this step? We need the snapshot value at push time .
[i] copies the current i into that iteration's closure. Why this step? By value freezes the loop counter as it stood on that pass.
Three independent closures store 0, 1, 2 respectively.
Calling them returns 0, 1, 2.
Now the trap: had we written [&i], all three would alias the same i. After the loop i is 3 (well, out of scope), so every call would read the same stale/dead value — a Cell-E-style bug.
Verify: fns[0]()=0, fns[1]()=1, fns[2]()=2. By value snapshots each iteration; by reference would have shared one doomed counter. ✔
Worked example Ex 8 — Cell H: real-world word problem (running total)
Problem. A cashier scans items with prices {40, 30, 55, 25}. Write one lambda that, called once per item, keeps a running total and returns it. What is the total after all four scans?
int total = 0 ;
auto scan = [ & total ]( int price ) { total += price; return total; };
scan ( 40 ); scan ( 30 ); scan ( 55 ); scan ( 25 );
std ::cout << total; // ?
Forecast: guess the four returned values and the final total.
We want the lambda to change the outside world (accumulate). Why this step? A snapshot can't accumulate — a photo never updates. We need the live phone line → [&total].
Scan 40: total = 0 + 40 = 40.
Scan 30: total = 40 + 30 = 70.
Scan 55: total = 70 + 55 = 125.
Scan 25: total = 125 + 25 = 150.
Verify (units): rupees added: 40 + 30 + 55 + 25 = 150. The reference-captured total holds 150. ✔
Worked example Ex 9 — Cell I: exam twist
[this] vs [*this]
struct Bank {
int balance = 500 ;
auto deposit_later () {
return [ this ]( int amt ) { balance += amt; return balance; };
}
};
Bank acc;
auto dep = acc. deposit_later ();
int r = dep ( 200 ); // ?
std ::cout << acc.balance; // ?
Forecast: predict r and acc.balance.
[this] captures the object's this pointer — by reference-like semantics. Why this step? Inside a method, balance really means this->balance; to touch it the lambda needs this. (See const member functions : a non-const method lets us modify balance.)
dep(200) runs acc.balance += 200 → 500 + 200 = 700, returns 700.
Because this points at the real acc, acc.balance is now 700.
The twist / danger: [this] is a pointer — if acc is destroyed before dep is called, it dangles exactly like Cell E. To carry a snapshot of the whole object , use [*this] (C++17), which copies the object into the closure. Then edits inside the lambda would leave acc.balance at 500.
Verify: with [this]: r = 700, acc.balance = 700. With [*this] the lambda would edit its own copy, leaving acc.balance = 500. ✔
Recall Every cell, one line each
By value after original changes ::: still the frozen snapshot value.
By reference after original changes ::: the current live value.
[x] mutable across two calls ::: the internal copy persists and keeps climbing; original never moves.
[=, &b] ::: default copy, but b aliased — copy edits stay inside, alias edits reach the real b.
[&local] returned from a function ::: dangling reference → undefined behaviour.
[] using an outside local ::: compile error; parameters are fine though.
[i] in a loop vs [&i] ::: value snapshots each iteration; reference shares one (doomed) counter.
[this] vs [*this] ::: pointer to the real object (can dangle) vs a full copy of the object.