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:

  1. std::move(data) produces an rvalue reference to data.
  2. captured_data is initialized using the move constructor of std::unique_ptr<int>.
  3. The original pointer data becomes nullptr.
  4. 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.