Lambda Captures

The Problem: Accessing External Variables

In the previous lesson, we introduced this example:

#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>

int main()
{
    constexpr std::array<std::string_view, 5> words{"sky", "blue", "green", "red", "rhythm"};

    auto result{std::find_if(words.begin(), words.end(),
                             [](std::string_view str) {
                                 return (str.find('a') != std::string_view::npos);
                             })};

    if (result == words.end())
        std::cout << "Not found\n";
    else
        std::cout << "Found: " << *result << '\n';

    return 0;
}

Now let's modify this to let the user choose which character to search for:

#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>

int main()
{
    constexpr std::array<std::string_view, 5> words{"sky", "blue", "green", "red", "rhythm"};

    std::cout << "Search for character: ";
    char target{};
    std::cin >> target;

    auto result{std::find_if(words.begin(), words.end(),
                             [](std::string_view str) {
                                 return str.find(target) != std::string_view::npos; // ERROR!
                             })};

    if (result == words.end())
        std::cout << "Not found\n";
    else
        std::cout << "Found: " << *result << '\n';

    return 0;
}

This won't compile! Unlike nested blocks where inner blocks can access outer variables, lambdas can only access certain kinds of identifiers defined outside the lambda:

  • Global variables and static local variables
  • Objects with static or thread-local storage duration
  • Constexpr objects (explicitly or implicitly)

Since target is a normal local variable, the lambda can't see it.

Tip
Lambdas can only access global variables, static locals, and constexpr objects defined outside the lambda.

To give the lambda access to target, we need a capture clause.

The Capture Clause

The capture clause (the [] part of a lambda) allows a lambda to access variables from the surrounding scope. We simply list the entities we want to access:

#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>

int main()
{
    constexpr std::array<std::string_view, 5> words{"sky", "blue", "green", "red", "rhythm"};

    std::cout << "Search for character: ";
    char target{};
    std::cin >> target;

    // Capture target by listing it in the capture clause
    auto result{std::find_if(words.begin(), words.end(),
                             [target](std::string_view str) {
                                 return str.find(target) != std::string_view::npos;
                             })};

    if (result == words.end())
        std::cout << "Not found\n";
    else
        std::cout << "Found: " << *result << '\n';

    return 0;
}

Sample output:

Search for character: e
Found: blue

How Captures Actually Work

Although it looks like the lambda is directly accessing main's target variable, it's not.

When a lambda is created, for each captured variable, the lambda makes a clone of that variable (with the same name) inside itself. The clone is initialized from the outer scope's variable.

In the example above, when the lambda object is created, it gets its own target member initialized from main's target. The lambda then uses its own copy.

Key Concept
Captured variables are copies of the outer scope variables, not the actual variables themselves. When you write [target], the lambda creates its own member variable named target that gets initialized from the outer target. The lambda then works with its own copy.

Advanced note: Lambdas are actually objects called functors. When the compiler sees a lambda definition, it creates a custom class. Each captured variable becomes a data member. When the lambda is created at runtime, these members are initialized.

Captures are Const by Default

By default, captured variables are treated as const inside the lambda:

#include <iostream>

int main()
{
    int score{100};

    auto losePoints{
        [score]() {
            --score; // ERROR: score is const

            std::cout << "Score: " << score << '\n';
        }
    };

    losePoints();

    return 0;
}

This won't compile because we're trying to modify a const variable.

Mutable Captures

To modify captured variables, mark the lambda as mutable:

#include <iostream>

int main()
{
    int score{100};

    auto losePoints{
        [score]() mutable {
            --score; // OK now

            std::cout << "Score: " << score << '\n';
        }
    };

    losePoints();
    losePoints();

    std::cout << "Actual score: " << score << '\n';

    return 0;
}

Output:

Score: 99
Score: 98
Actual score: 100

Wait - the actual score is still 100! The lambda modified its own copy of score, not the original in main. Note that the lambda's copy persists across calls (98 in the second call, not 99).

Warning
Because captured variables are members of the lambda object, their values persist across multiple lambda calls.

Capture by Reference

To allow the lambda to modify the original variable, capture by reference using &:

#include <iostream>

int main()
{
    int score{100};

    auto losePoints{
        [&score]() { // & means capture by reference
            --score; // modifies main's score

            std::cout << "Score: " << score << '\n';
        }
    };

    losePoints();

    std::cout << "Actual score: " << score << '\n';

    return 0;
}

Output:

Score: 99
Actual score: 99

Now the lambda modifies the original variable. Reference captures are not const (unless the captured variable itself is const).

Prefer reference captures for non-fundamental types (like you would prefer passing by reference to functions).

Example - counting comparisons:

#include <algorithm>
#include <array>
#include <iostream>
#include <string_view>

struct Book
{
    std::string_view title{};
    std::string_view author{};
};

int main()
{
    std::array<Book, 4> books{
        {{"1984", "Orwell"},
         {"Brave New World", "Huxley"},
         {"Fahrenheit 451", "Bradbury"},
         {"Animal Farm", "Orwell"}}
    };

    int comparisons{0};

    std::sort(books.begin(), books.end(),
              [&comparisons](const auto& a, const auto& b) {
                  ++comparisons; // modifies main's comparisons

                  return a.author < b.author;
              });

    std::cout << "Comparisons made: " << comparisons << '\n';

    for (const auto& book : books)
        std::cout << book.author << ": " << book.title << '\n';

    return 0;
}

Possible output:

Comparisons made: 5
Bradbury: Fahrenheit 451
Huxley: Brave New World
Orwell: 1984
Orwell: Animal Farm

Capturing Multiple Variables

Separate multiple captures with commas. You can mix value and reference captures:

int health{100};
int armor{50};
int experience{0};

// Capture health and armor by value, experience by reference
[health, armor, &experience](){};

Default Captures

Explicitly listing every variable can be tedious. A default capture automatically captures all variables mentioned in the lambda.

  • [=] captures all used variables by value
  • [&] captures all used variables by reference

Example:

#include <algorithm>
#include <array>
#include <iostream>

int main()
{
    std::array temperatures{18, 22, 25, 19, 30, 28};

    int minTemp{};
    int maxTemp{};

    std::cout << "Enter min and max temperature: ";
    std::cin >> minTemp >> maxTemp;

    auto inRange{std::find_if(temperatures.begin(), temperatures.end(),
                              [=](int temp) { // captures minTemp and maxTemp by value
                                  return temp >= minTemp && temp <= maxTemp;
                              })};

    if (inRange == temperatures.end())
        std::cout << "No temperatures in range\n";
    else
        std::cout << "Found: " << *inRange << '\n';

    return 0;
}

You can mix default captures with specific captures:

int health{100};
int armor{50};
int experience{0};

[health, armor, &experience](){}; // explicit

[=, &experience](){}; // capture everything by value except experience (by reference)

[&, armor](){}; // capture everything by reference except armor (by value)

[&, &armor](){}; // ERROR: already capturing by reference

[=, armor](){}; // ERROR: already capturing by value

[armor, &health, &armor](){}; // ERROR: armor listed twice

[armor, &](){}; // ERROR: default capture must be first

Capture Mode Comparison

Syntax Meaning When to Use
[x] Capture x by value (const copy) Read-only access to small types
[&x] Capture x by reference Need to modify original, or large objects
[=] Capture all used variables by value Multiple read-only captures
[&] Capture all used variables by reference Multiple variables need modification
[=, &x] Default by value, but x by reference Most by value, some by reference
[&, x] Default by reference, but x by value Most by reference, some by value
[x{expr}] Init capture: create x from expression Computed or transformed captures
[this] Capture this pointer Access member variables/functions
[*this] Capture copy of *this (C++17) Safe copy when object may be destroyed
Key Concept
The choice between value and reference capture is similar to choosing between pass-by-value and pass-by-reference for function parameters. Use value captures for small, read-only data. Use reference captures when you need to modify the original or when copying would be expensive.

Defining Variables in the Capture

Sometimes you want to capture a modified version of a variable or define a new variable only visible to the lambda:

#include <algorithm>
#include <array>
#include <iostream>

int main()
{
    std::array temperatures{18, 22, 25, 19, 30, 28};

    int width{};
    int height{};

    std::cout << "Enter width and height: ";
    std::cin >> width >> height;

    auto found{std::find_if(temperatures.begin(), temperatures.end(),
                            // Define targetTemp visible only to lambda
                            [targetTemp{width * height}](int temp) {
                                return temp == targetTemp;
                            })};

    if (found == temperatures.end())
        std::cout << "Temperature not found\n";
    else
        std::cout << "Found: " << *found << '\n';

    return 0;
}

targetTemp is calculated once when the lambda is defined and stored in the lambda object. If the lambda is mutable and modifies this variable, the original value is lost.

Best Practice
Only initialize variables in the capture if the value is short and the type is obvious. Otherwise define the variable outside the lambda and capture it.

Dangling Captured References

Variables captured by reference must outlive the lambda. Returning a lambda that captures local variables by reference creates dangling references:

#include <iostream>
#include <string>

auto makeGreeter(const std::string& greeting)
{
    // Returns a lambda that captures greeting by reference
    return [&]() {
        std::cout << greeting << '\n'; // UNDEFINED BEHAVIOR
    };
}

int main()
{
    auto greet{makeGreeter("Hello")};

    greet(); // greeting no longer exists!

    return 0;
}

The temporary std::string created from "Hello" is destroyed when makeGreeter returns, but the lambda still references it.

Warning
Be careful when capturing by reference, especially with default reference captures. Captured variables must outlive the lambda.

Common Dangling Reference Scenarios

Dangling references occur most often in these situations:

  1. Returning lambdas from functions (as shown above)
  2. Storing lambdas in containers or member variables that outlive the captured locals
  3. Passing lambdas to asynchronous operations (threads, callbacks) where the lambda runs after the original scope ends
  4. Using [&] default capture when only some variables need to be references
class Button
{
public:
    std::function<void()> onClick{};

    void setClickHandler(int localValue)
    {
        // DANGEROUS: localValue dies when setClickHandler returns
        onClick = [&localValue]() {
            std::cout << localValue << '\n'; // UNDEFINED BEHAVIOR
        };
    }
};

To fix this, capture by value instead:

auto makeGreeter(const std::string& greeting)
{
    return [greeting]() { // capture by value
        std::cout << greeting << '\n';
    };
}

Unintended Copies of Mutable Lambdas

Because lambdas are objects, they can be copied. This can cause problems with mutable lambdas:

#include <iostream>

int main()
{
    int counter{0};

    auto increment{[counter]() mutable {
        std::cout << ++counter << '\n';
    }};

    increment(); // 1

    auto otherIncrement{increment}; // copy increment

    increment();      // 2
    otherIncrement(); // 2 (not 3!)

    return 0;
}

Output:

1
2
2

When we copied increment, we copied it in its current state (counter == 1). Now there are two independent lambdas, each with their own counter.

This also happens when passing lambdas to functions. Consider:

#include <iostream>
#include <functional>

void invoke(const std::function<void()>& fn)
{
    fn();
}

int main()
{
    int counter{0};

    auto increment{[counter]() mutable {
        std::cout << ++counter << '\n';
    }};

    invoke(increment);
    invoke(increment);
    invoke(increment);

    return 0;
}

Output:

1
1
1

When we call invoke(increment), the compiler converts increment to a temporary std::function, creating a copy. Each call to invoke makes a new copy, so each sees counter == 0.

Solutions

Option 1: Store the lambda in a std::function from the start:

#include <iostream>
#include <functional>

void invoke(const std::function<void()>& fn)
{
    fn();
}

int main()
{
    int counter{0};

    std::function increment{[counter]() mutable {
        std::cout << ++counter << '\n';
    }};

    invoke(increment); // no copy
    invoke(increment); // no copy
    invoke(increment); // no copy

    return 0;
}

Output:

1
2
3

Option 2: Use std::ref() to pass a reference wrapper:

#include <iostream>
#include <functional>

void invoke(const std::function<void()>& fn)
{
    fn();
}

int main()
{
    int counter{0};

    auto increment{[counter]() mutable {
        std::cout << ++counter << '\n';
    }};

    invoke(std::ref(increment)); // pass reference wrapper
    invoke(std::ref(increment));
    invoke(std::ref(increment));

    return 0;
}

Output:

1
2
3

std::ref() creates a std::reference_wrapper that copies the reference instead of the lambda itself.

Rule
Standard library functions may copy lambdas. For mutable lambdas with captures, pass them by reference using `std::ref`.
Best Practice
Avoid mutable lambdas when possible. Non-mutable lambdas are easier to understand, don't have copying issues, and work better with parallel execution.

Quiz

Question 1: Which variables can the lambda use without explicitly capturing them?

int globalVar{};
static int staticGlobal{};

int getValue()
{
    return 0;
}

int main()
{
    int localVar{};
    constexpr int constLocal{};
    static int staticLocal{};
    static constexpr int staticConstLocal{};
    const int constLocalA{};
    const int constLocalB{getValue()};
    static const int staticConstLocalA{};
    static const int staticConstLocalB{getValue()};

    [](){
        globalVar;
        staticGlobal;
        localVar;
        constLocal;
        staticLocal;
        staticConstLocal;
        constLocalA;
        constLocalB;
        staticConstLocalA;
        staticConstLocalB;
    }();

    return 0;
}
Show Solution
Variable Usable without capture
globalVar Yes (global)
staticGlobal Yes (static storage)
localVar No (automatic storage)
constLocal Yes (usable in constant expression)
staticLocal Yes (static storage)
staticConstLocal Yes
constLocalA Yes (usable in constant expression)
constLocalB No (depends on runtime function call)
staticConstLocalA Yes
staticConstLocalB Yes (static storage)

Question 2: What does this code print?

#include <iostream>
#include <string>

int main()
{
    std::string favoriteFood{"pizza"};

    auto printFood{
        [=]() {
            std::cout << "I like " << favoriteFood << '\n';
        }
    };

    favoriteFood = "sushi";

    printFood();

    return 0;
}
Show Solution
I like pizza

printFood captured favoriteFood by value. Modifying main's favoriteFood doesn't affect the lambda's copy.

Question 3: (More complex - see original lesson for full quiz)

We're building a number guessing game:

Setup:

  • Ask for a starting number
  • Ask how many values to generate
  • Pick a random multiplier (2-4)
  • Generate square numbers starting from the input, each multiplied by the multiplier

Gameplay:

  • User guesses values
  • If guess matches, remove it and continue
  • If guess is wrong, user loses and program shows nearest value
  • If user guesses all values, they win

Use std::vector to store numbers, std::find to search, std::vector::erase to remove matches, std::min_element with a lambda to find the closest value.

Show Hint

Use std::abs from <cmath> to calculate distance:

int distance{std::abs(7 - 10)}; // 3
Show Solution

See the original lesson for the complete solution - it's quite long!

Summary

Capture clause: The [] part of a lambda that allows it to access variables from the surrounding scope. Without captures, lambdas can only access global variables, static locals, and constexpr objects.

Capture by value: Listing a variable in the capture clause (e.g., [target]) creates a copy of that variable inside the lambda. The copy is const by default and persists across multiple lambda calls.

Mutable captures: Adding the mutable keyword allows modifying captured variables, but changes only affect the lambda's copy, not the original variable.

Capture by reference: Using & (e.g., [&score]) captures a reference to the original variable, allowing the lambda to read and modify it directly. Reference captures are not const.

Default captures: [=] captures all used variables by value, while [&] captures all used variables by reference. Can be combined with specific captures (e.g., [=, &experience]).

Init captures: Variables can be defined directly in the capture clause (e.g., [targetTemp{width * height}]), creating variables visible only to the lambda.

Dangling references: Variables captured by reference must outlive the lambda. Returning a lambda that captures local variables by reference creates undefined behavior when those locals are destroyed.

Mutable lambda copying problem: When mutable lambdas are copied (explicitly or when passed to standard library functions), each copy gets its own state. Use std::function from the start or pass with std::ref() to avoid unintended copies.

Best practices: avoid mutable lambdas when possible (they're harder to understand and have copying issues), prefer capturing by reference for non-fundamental types, and always ensure reference-captured variables outlive the lambda. For variable-length argument lists or complex state management, consider alternatives to lambdas.