Move-Only Types in C++ Lambdas Using Init-Captures
This article explains how C++14 generalized lambda capture, commonly
known as init-capture, enables developers to capture move-only types
inside closures. Prior to C++14, lambdas were restricted to capturing
variables by copy or by reference, making it impossible to transfer
ownership of types like std::unique_ptr or
std::future directly into a closure. By introducing custom
initialization expressions into the capture clause, init-captures allow
objects to be moved into the closure object, solving lifetime and
ownership challenges cleanly and efficiently.
The Limitation in C++11
In C++11, lambdas support two primary capture modes:
- Capture by value (
[x]): Copies the variable into the closure object. - Capture by reference (
[&x]): Stores a reference to the variable inside the closure object.
Move-only types (such as std::unique_ptr,
std::thread, or std::promise) have deleted
copy constructors. Attempting to capture them by value produces a
compilation error because copy construction is required. Conversely,
capturing them by reference does not transfer ownership. If the lambda
outlives the scope in which the move-only object was defined—such as
when dispatching a task to a background thread or an event queue—the
captured reference dangles, leading to undefined behavior.
How Init-Captures Work
C++14 introduced init-capture (generalized lambda capture), which allows you to define a new member variable inside the closure and initialize it with an arbitrary expression:
[capture_name = expression](/* parameters */) {
/* body */
}The identifier to the left of the = becomes a member
variable of the generated closure class. The expression to the right of
the = is evaluated in the scope where the lambda is
created.
Because the right-hand side can be any valid expression, you can wrap
a variable in std::move(). This casts the source object to
an rvalue, invoking the move constructor rather than the copy
constructor when initializing the closure's internal variable.
Capturing a Move-Only Type: Code Example
Consider managing a dynamically allocated resource via
std::unique_ptr:
#include <iostream>
#include <memory>
#include <utility>
int main() {
auto data = std::make_unique<int>(42);
// Init-capture moves 'data' into 'captured_data' within the closure
auto task = [captured_data = std::move(data)]() {
std::cout << "Value: " << *captured_data << '\n';
};
// 'data' is now null because ownership was moved into the closure
if (!data) {
std::cout << "Original pointer is null.\n";
}
task();
return 0;
}In this example:
std::move(data)produces an rvalue reference todata.captured_datais initialized using the move constructor ofstd::unique_ptr<int>.- The original pointer
databecomesnullptr. - The closure now holds sole ownership of the allocated integer, ensuring the resource remains valid as long as the lambda object exists.
Variable Shadowing
The variable name inside the closure does not need to differ from the original variable name. You can write:
auto task = [data = std::move(data)]() {
std::cout << *data << '\n';
};Here, the left data lives inside the closure’s scope,
while the right data refers to the variable in the
enclosing scope.
The Role of mutable
By default, a lambda’s function call operator
(operator()) is marked const. If you need to
modify the captured move-only object—or move it out of the lambda into
another function—the lambda must be declared mutable:
#include <memory>
#include <utility>
void process(std::unique_ptr<int> ptr);
int main() {
auto ptr = std::make_unique<int>(100);
auto consumer = [ptr = std::move(ptr)]() mutable {
// Without 'mutable', std::move(ptr) would yield a const rvalue,
// preventing the non-const move constructor from executing.
process(std::move(ptr));
};
consumer();
}Adding mutable removes the const qualifier
from the closure’s operator(), allowing move operations and
mutating member function calls on captured objects.