Visual walkthrough — Lambda expressions — capture list (by value, by reference)
This is the picture-derivation of the parent idea in the capture-list topic.
Step 1 — A variable is just a labelled box in memory
WHAT. Before any lambda exists, we have a plain local variable.
int n = 10;WHY start here. Everything about "by value" vs "by reference" is a story about where a number physically lives. We cannot talk about copying a box or pointing at a box until we can see the box.
PICTURE. Look at the figure: n is a small box in the function's stack frame. The label n is just a name a human uses; the box itself holds the pattern 10. Its location — call it an address (the box's street number) — is the thing we will later copy or point at.
- ::: the name we type in code
- the box ::: the actual memory that stores
- the address ::: the box's location, which references will remember

Step 2 — A lambda is secretly a struct being built
WHAT. When you write a lambda, the compiler invents a hidden class (a closure type) and makes one object of it.
auto f = [/* capture */](int x){ return x + n; };WHY. A function on its own has no memory between calls. But this lambda needs to remember n. The only way to attach memory to a callable is to make it an object with fields — a struct. So the compiler writes one for you. The capture list decides what fields go inside.
PICTURE. The figure shows two things side by side: your one line of code, and the hidden struct the compiler generates. The struct has a special method operator() — the code that runs when you write f(...). The empty slot at the top is where captures will land in the next steps.

Step 3 — Capture by value [n]: the compiler copies the box into the struct
WHAT. Writing [n] adds a field int n; to the struct and copies the current value in at creation time.
int n = 10;
auto f = [n](int x){ return x + n; }; // copy made HERE, n is 10The generated struct:
WHY by value here. We want the lambda to be portable and safe — a self-contained thing that carries its own data. A copy achieves that: the struct now owns a completely separate box.
PICTURE. Follow the mint arrow: the value 10 flows from the outer box into a new, second box that lives inside the struct f. Two boxes now hold 10. They are unrelated from this moment on.

Step 4 — Change the original: the by-value copy does not flinch
WHAT. We mutate the outer variable after the copy was made.
n = 99; // edits the OUTER box only
std::cout << f(0); // prints 10, not 99WHY this case matters. This is the whole point of "snapshot." The copy happened in Step 3 while n was 10. Assigning n = 99 writes into the outer box; the struct's inner box was never wired to it.
PICTURE. The coral arrow writes 99 into the outer box. The inner box (inside f) still reads 10 — no arrow reaches it. Term by term when f(0) runs:

Step 5 — Capture by reference [&n]: the struct stores the address, not a copy
WHAT. Writing [&n] adds a field int& n; — a reference, which is another name for the very same box.
int n = 10;
auto g = [&n](int x){ return x + n; }; // struct stores &n (the address)The generated struct:
WHY a reference here. Sometimes we want the lambda to talk to the live original — to read its latest value or to change it. A reference does exactly that: it keeps no data of its own, it just remembers where the real data is. Compare with references vs pointers — a reference is the pointer's polite twin: same "point at a box" idea, no * needed.
PICTURE. No second box appears this time. Instead a lavender arrow leaves the struct and points back at the one outer box. The struct is a signpost, not a warehouse.

Step 6 — Change the original: the by-reference lambda sees it instantly
WHAT. Mutate the outer variable, then call the lambda.
n = 99;
std::cout << g(0); // prints 99WHY. g never stored 10. It stored a route to the box. Reading n inside g means "follow the arrow, read whatever is there now." We just wrote 99 into that exact box.
PICTURE. The coral arrow writes 99 into the outer box; the lavender signpost from g still points at that same box, so it now reads 99. Term by term:

Step 7 — The mutable case: editing a by-value copy without touching the original
WHAT. By default operator() is const, so a by-value field is read-only. mutable unlocks it.
int n = 10;
auto c = [n]() mutable { n++; return n; };
c(); // 11
c(); // 12 (the inner box persists between calls)
n; // 10 (outer box never moved)WHY this edge case. People assume "I named n, so I can change n." But changing the copy is only legal after you drop the const guard with mutable. Even then, you edit the struct's own box, never the outer one — there is no arrow between them (Step 3 severed them).
PICTURE. The ++ acts only on the inner box: 10 → 11 → 12 across two calls. The outer box sits untouched at 10. See also const member functions — that same const on operator() is what forbids the edit until you say mutable.

Step 8 — The degenerate case: the reference outlives its box (dangling)
WHAT. A by-reference lambda is returned out of the function whose local it points at.
auto make() {
int x = 42;
return [&x]() { return x; }; // BUG
}
auto bad = make();
bad(); // undefined behaviour — the box is goneWHY this is the most important case. Step 5 taught us the reference is just an address. When make() returns, its stack frame is torn down — the box at that address is demolished. The signpost inside bad now points at rubble. This ties directly to object lifetime and scope and matters most with threads and async, where lambdas routinely outlive the code that spawned them. Compare with how std::function stores such a closure long after its birthplace has vanished.
PICTURE. Left frame: inside make(), the box x = 42 exists and the arrow is valid. Right frame: make() has returned, the frame is greyed out, the box is gone, and the arrow points into empty space — the crash.
Fix. Capture by value ([x] or [=]) so the value lives inside the closure and travels with it — no arrow to demolish.

The one-picture summary
Everything above collapses into one contrast: value copies the box in; reference stores an arrow out. A copy is portable but frozen. An arrow is live but only valid while its box still stands.

Recall Feynman retelling — the whole walkthrough in plain words
Start with a box named n holding 10 (Step 1). When you write a lambda, the compiler quietly builds a little robot — a struct with a "do it" button, operator() (Step 2). Now the question is what the robot carries. If you wrote [n], the compiler pours a copy of 10 into a pocket inside the robot (Step 3). Change the outside n to 99 and the robot shrugs — its pocket still holds 10 (Step 4). If instead you wrote [&n], the robot carries no number at all, just a note saying "the number lives over there" (Step 5). Change the outside n to 99 and the robot now reports 99, because it re-reads the box every time (Step 6). By default the robot's button is sealed const, so it can't scribble on its own pocket copy — say mutable and now it can, but only its own copy, never the real box (Step 7). The one deadly trap: if the robot's note points at a box that gets demolished when its function ends, the robot is holding directions to a house that no longer exists — follow them and you crash (Step 8). Carrying it away? Put the real thing in your pocket (value). Staying home and wanting the freshest value? Keep the note (reference).
After [n] copies, are the outer box and the struct's inner box connected?
What does a [&n] closure physically store?
Why is operator() being const relevant to by-value edits?
[n]() { n++; } is illegal until you add mutable.Does mutable let you change the original captured variable?
What causes a dangling reference from a returned lambda?
[&x] stores the address of a local; when the function returns its frame is destroyed, so the address points at reclaimed memory (UB).