Lambdas and Callable Objects

Lambda Capture Lifetime Problems

A lambda that captures a variable by reference does not own that variable; it simply contains a reference to the original object. If the original object ceases to exist while the lambda still exists, later references to the lambda's reference will access storage that no longer belongs to the program. The result is undefined behavior.

The problem is easiest to see when a lambda is returned from a function. The local variable that was captured is dead when the function returns, but the lambda returned still refers to it.

#include <iostream>

auto makeLambda()
{
    int local{ 42 };
    // Capture this value by reference in the lambda
    return [&local]() { return local; };
}

int main()
{
    auto fn{ makeLambda() };
    std::cout << fn() << '\n';
    return 0;
}

Within makeLambda, the local variable local gets created and then captured by reference. The lambda is returned to main and stored in a variable named fn. At the exact point in time the makeLambda function returns, the local variable local is destroyed.

The lambda now contains a reference to the now non existent local variable.

When the value of the lambda is used in the code at some later point, the code attempts to read from invalid memory. The output of the program might be any random value, the program might crash, or just by chance give the output it was expecting. This, in fact, is undefined behavior.

Capture by value helps to prevent that. Inside of the lambda, we now have a copy of the variable, so the lambda is no longer related to the lifetime of the original anymore.

#include <iostream>

auto makeLambda()
{
    int local{ 42 };
    return [local]() { return local; };
}

int main()
{
    // Initialize fn with the value returned by makeLambda
    auto fn{ makeLambda() };
    std::cout << fn() << '\n';
    return 0;
}

The capture list [local] makes a private copy of 42. That copy will sit happily in the function object that is returned. When local is destroyed, the copy will be unaffected.

So fn() happily returns 42 and we print out that value.

The lambda is thus self contained.

The same lifetime rule applies when you store a lambda for later use instead of returning it. If you capture a local variable by reference, that local variable is destroyed when the method returns.

#include <functional>
#include <iostream>

int main()
{
    std::function<int()> fn;

    {
        int local{ 99 };
        // Capture this local value by reference in the lambda
        fn = [&local]() { return local; };
    }

    std::cout << fn() << '\n';
    return 0;
}

The inner block makes a local and then saves a lambda captured by reference in the std::function object fn. When the block exits, local is destroyed. The std::function still has the lambda, and so the next call to fn() reaches into dead storage. Behavior is undefined.

Capturing by value gets rid of the dependency.

#include <functional>
#include <iostream>

int main()
{
    std::function<int()> fn;

    {
        int local{ 99 };
        fn = [local]() { return local; };
    }

    std::cout << fn() << '\n';
    return 0;
}

The lambda now holds its own copy of 99. When the block exits, the copy still lives within the std::function, so the call is valid and we get 99 printed out.

Reference capture isn't always an error. If the lambda is called only as long as the original variable is in scope, then the reference is valid, and this often is slightly more efficient.

#include <iostream>

int main()
{
    int value{ 10 };

    auto print{ [&value]() { std::cout << value << '\n'; } };

    print();
    value = 20;
    print();

    return 0;
}

The lambda is constructed, used, and destroyed while value still exists. Both calls are safe. The first outputs 10, the second outputs 20, because the reference sees the updated value. The lifetime of the lambda never extends beyond the lifetime of the referenced variable.

Thus, the crucial issue is always the same: is the lambda still present when the captured object is destroyed? If yes, capture by value (or make sure the original object lives long enough). 

If no, capturing by reference is both safe and correct. Any use of a lambda that captures by reference long after the object it references has been destroyed is undefined behavior.

Exercise: Lambda Capture Lifetime ProblemsStarter code, hints and solutionOpenClose
Guided practiceBeginner · 10–15 min

Practice: Lambda Capture Lifetime Problems

Work through the mission, reveal hints only when needed, and compare your result with the model solution.

Your challenge

Change local after the first lambda captures it and test fn after the block. Then demonstrate a second value changed before capture, and compare the stored results without dangling references.

Mission checklist

  1. Run the starter as a baseline, then predict the change requested in Your challenge.
  2. Make that change in your editor; use the lesson-specific hints if you need help.
  3. Compile and run your answer, then compare with the worked solution and explain the result.

Starter program

This is the working baseline, not the completed answer. Compile as C++20, then make the change described above.

#include <functional>
#include <iostream>

int main()
{
    std::function<int()> fn;

    {
        int local{ 99 };
        // Keep a copy of local in the lambda so it survives the block
        fn = [local]() { return local; };
    }

    std::cout << fn() << '\n';
    return 0;
}

Hints

Hint 1

The original local disappears at the inner closing brace, but [local] stores a separate copy in the lambda held by fn. That copy has the closure's lifetime, not the inner block's lifetime.

Hint 2

Change local before creating the lambda to change the captured value. An assignment to the original local afterward does not update the copy; using [&local] would instead leave a dangling reference after the block.

Worked solution

local is changed to 100 after the first lambda captures its own copy of 99, so fn still returns 99. A second block changes a value to 77 before creating another value-capturing lambda, so that closure later returns 77. Both calls are safe after the original locals expire.

#include <functional>
#include <iostream>

int main()
{
    std::function<int()> fn;

    {
        int local{ 99 };
        // Keep a copy of local in the lambda so it survives the block
        fn = [local]() { return local; };
        local = 100;
    }

    std::cout << fn() << '\n';
    std::function<int()> before;
    {
        int value{88};
        value = 77;
        before = [value]() { return value; };
    }
    std::cout << "Captured after change: " << before() << '\n';
    return 0;
}

Example output

99
Captured after change: 77

0 comments

No comments yet. Be the first.

Add a comment

About CppLearn

CppLearn is an interactive, free reference for modern C++. In each lesson, you'll find out how the language works, rather than just lines of code that you can copy and paste. We cover concepts starting from beginner lessons on fundamentals and proceed to templates, move semantics, smart pointers, and areas that usually trap or scare C++ learners.