50 / 163 · C++14 · 9 min
Lambda capture: a closure is an object with a lifetime
A lambda creates a closure object with a call operator; capture decides whether it stores a value or borrows outer state. Capture by value, by reference, and init-capture have different ownership consequences. Before storing a callback, prove that the objects it depends on live until the call finishes.
In this lesson
Capture is the design of the closure’s data members
A C++11 lambda is not an anonymous code fragment outside the object model. The compiler produces a unique closure type for each lambda expression, and captured state is stored with the closure object. [n] saves n’s value when the closure is created; [&n] borrows the original object. Later changes to the outer n are necessarily observed only by the borrowing version.
The call operator of an ordinary by-value capture is const by default, so the captured copy cannot be modified directly. Adding mutable allows changing the copy, but those changes are not synced back to the outer n. The example increments the copy and the original separately, showing that mutable changes the mutability of the closure itself.
Init-capture can transfer ownership
C++14 added init-capture, for example [p = std::move(owner)], which is suitable for moving unique_ptr ownership into a callback. The closure can therefore become a move-only, non-copyable type. Generic lambdas with auto parameters also start in C++14; behind them is still a templated call operator, not a run-time container of arbitrary types.
The example puts the exclusive owner of an integer into a lambda and safely reads the integer owned by the closure after the source pointer is empty. That is a better fit than capturing a reference to a local unique_ptr when the task must leave the current scope. Note that copying a raw pointer or a string_view only copies the borrow; it does not automatically copy the data they point to.
Storing and asynchronous calls need extra checks
Reference capture does not extend an object’s lifetime; a lambda that returns after capturing a reference to a local variable leaves a dangling borrow. In a member function, [this] stores a pointer, not the whole object; default capture by value does not by itself keep the object alive. C++17’s [*this] copies the object, but the business meaning of that copy still needs review.
Small algorithms that run immediately can usually borrow local data clearly; callbacks kept for a long time should list captures explicitly, making value semantics, ownership, or outer lifetime constraints obvious. Call wrappers have limits too: C++20’s std::function requires a copyable target and cannot directly accept the unique_ptr-owning closure in this example.
Pitfalls
- mutable only allows modifying the by-value captured copy; it does not turn that copy into a reference to the outer variable.
- [=] does not mean a recursive deep copy; when this, a raw pointer, or a view is captured, the underlying object can still be destroyed before the callback runs.
Run an example
Minimum C++14 · complete program · Download .cpp
#include <cassert>
#include <iostream>
#include <memory>
#include <utility>
int main() {
int n = 5;
auto own_copy = [n]() mutable { return ++n; };
auto borrow = [&n] { return ++n; };
const int a = own_copy();
assert(a == 6 && n == 5);
const int b = borrow();
assert(b == 6 && n == 6);
auto p = std::make_unique<int>(9);
auto owns_resource = [p = std::move(p)] { return *p; };
assert(!p && owns_resource() == 9);
std::cout << a << ' ' << b << ' ' << owns_resource() << '\n';
}
Compile locally
g++ -std=c++14 -Wall -Wextra -Wpedantic -pthread modern-lambda.cpp -o example && ./exampleExpected result
6 6 9
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
Without changing the structure of the example, call own_copy() once more. What does it return? If n is set to 100 before that, does the result change?
Show a reference answer
It returns 7. The first call already changed the closure’s internal copy from 5 to 6; the second call increments it again. Setting the outer n to 100 does not affect the copy. borrow() would access the outer n and turn it into 101; that is the difference between the two capture strategies.
Check the sources
Drafts and official chapters change. The version mark is only the example’s minimum.