Skip to content

66 Intermediate The Hundred

The Lambda That Outlived Its Capture

the counter that lost count

#include <iostream>

auto make_counter() {
    int count = 0;
    return [&count] { return ++count; };
}

void log_progress() {
    int steps[8] = {11, 22, 33, 44, 55, 66, 77, 88};
    std::cout << "  (progress: step " << steps[7] << ")\n";
}

int main() {
    auto next = make_counter();   // a fresh counter: 1, 2, 3, ...
    std::cout << next() << "\n";
    log_progress();
    std::cout << next() << "\n";
    std::cout << next() << "\n";
}

Run it. Does it count 1, 2, 3?

Answer

No. Typical output (GCC on x86-64): the same garbage number three times — 32767, 32767, 32767 — and a slightly different number on the next run. This is undefined behavior: on another setup it may even appear to work.

Why

[&count] stores a reference — under the hood, a pointer — to a variable living in make_counter's stack frame. The moment make_counter returns, that frame is gone, but the lambda survives in main, still pointing at the dead slot; every next() is now a read-modify-write of reclaimed memory — undefined behavior. Worse, ordinary calls like log_progress and operator<< reuse that same stack region between reads, which is why the counter doesn't even advance: each ++count increments freshly restomped garbage. GCC 11 compiles this without a peep, even with -Wall -Wextra. AddressSanitizer catches it at runtime, though: -fsanitize=address plus ASAN_OPTIONS=detect_stack_use_after_return=1 reports stack-use-after-return inside the lambda's operator(). Capturing this has the same failure mode with members — that's entry 78.

The fix

Give the closure its own copy — capture by value, or drop the local entirely with an init-capture (mutable lets the lambda modify what it owns):

return [count]() mutable { return ++count; };      // copy of the local
return [count = 0]() mutable { return ++count; };  // init-capture: no local at all

Takeaway: a lambda that outlives the current scope must never capture locals by reference — if the closure needs it later, the closure must own it.

Try it: g++ -std=c++17 main.cpp -o demo && ./demo

Open in Compiler Explorer ↗ Quiz this entry