Lambda Capture by Value and Reference in C++

Lambda expressions in modern C++ allow anonymous functions to capture variables from their enclosing scope, determining how data outside the lambda is accessed and managed. This article explains how variable capture works in C++, focusing on capture by value versus capture by reference, the syntax for explicit and default captures, the behavior of the mutable keyword, and best practices for avoiding common pitfalls such as dangling references.


The Capture Clause

A lambda expression introduces variable captures through its capture clause, denoted by brackets [] at the beginning of the expression. Variables within the surrounding lexical scope can be captured explicitly by name or implicitly via capture defaults.

[capture_list](parameters) -> return_type {
    // function body
};

Capture by Value

When a variable is captured by value, the lambda creates an independent copy of that variable at the moment the lambda is defined.

Syntax

To capture a variable x by value, include its name inside the brackets without an ampersand:

int x = 10;
auto lambda = [x]() {
    return x * 2;
};

Immutability and the mutable Keyword

By default, the function call operator (operator()) of a lambda is marked const. This means captured-by-value variables cannot be modified inside the lambda body.

To modify the local copy of a captured-by-value variable, declare the lambda as mutable:

int count = 0;
auto increment = [count]() mutable {
    count++; // Modifies the local copy, not the external 'count'
    return count;
};

increment(); // returns 1
increment(); // returns 2
// The external 'count' variable remains 0.

Advantages and Risks

  • Advantage: Safe for asynchronous or delayed execution (e.g., thread pools or callbacks) because the lambda owns its own copy of the data.
  • Risk: Copying large objects can cause performance overhead.

Capture by Reference

When a variable is captured by reference, the lambda stores a reference to the original variable rather than making a copy. Any reads or writes within the lambda directly interact with the original object.

Syntax

To capture a variable by reference, prefix the variable name with an ampersand &:

int total = 0;
auto addToTotal = [&total](int amount) {
    total += amount; // Directly modifies the external 'total'
};

addToTotal(5); // 'total' is now 5

Because the lambda operates on references, modifying the variable does not require the mutable specifier unless the referenced variable itself is const.

Advantages and Risks

  • Advantage: Zero copying overhead, making it ideal for passing heavy objects or accumulating state.
  • Risk (Dangling References): If a lambda outlives the scope of the captured variable (such as returning a lambda from a function or passing it to an asynchronous worker), accessing the captured reference causes undefined behavior.

Default Captures

C++ provides shorthand syntax to capture all referenced external variables automatically:

  • [=] (Default Capture by Value): Implicitly captures any variable used within the lambda body by value.
  • [&] (Default Capture by Reference): Implicitly captures any variable used within the lambda body by reference.

Mixed Captures

You can combine a default capture mode with explicit exceptions:

int a = 1, b = 2, c = 3;

// Capture everything by value, but capture 'c' by reference
auto lambda1 = [=, &c]() {
    c = a + b;
};

// Capture everything by reference, but capture 'a' by value
auto lambda2 = [&, a]() {
    b += a;
};

Note: You cannot use the same capture mode explicitly that is already established by the default (e.g., [=, a] is invalid).


Summary of Differences

Feature Capture by Value ([x]) Capture by Reference ([&x])
Data Mechanism Stores an internal copy Stores a reference to the original
Original Variable Remains unchanged Modified by changes inside the lambda
Default Mutability Read-only (requires mutable to edit) Read/Write (if original is non-const)
Lifetime Safety Safe across scope boundaries Vulnerable to dangling references
Performance Incurs copy cost for large types Zero copy cost