5.2.22 · D4C++ Programming

Exercises — Lambda expressions — capture list (by value, by reference)

2,266 words10 min readBack to topic

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]a by value.
  • [&a]a by reference.
  • [=] → all used (a,b,c) by value.
  • [&] → all used (a,b,c) by reference.
  • [=, &b] → default by value (a,c copies), but b by reference.
  • [&, a] → default by reference (b,c aliases), but a by value.

Why: the leading = or & sets the default; each explicit entry after it is the exception.

Recall Solution 1.2
  • p uses [n]photo (frozen snapshot of n = 5).
  • q uses [&n]phone line (live wire to the real n).

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: hits 0→1; copy-base 100→101; returns 101.
  • Call 2: hits 1→2; copy-base 101→103; returns 103.
  • Line Z: base = 0 edits the original base — but the lambda holds a separate copy, so its base is still 103. No effect on the closure.
  • Call 3: hits 2→3; copy-base 103→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 aliased

Why 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.

Figure — Lambda expressions — capture list (by value, by reference)

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

Figure — Lambda expressions — capture list (by value, by reference)

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).