C++ Lambda Expression Closure Traps

Lambda expressions are a powerful feature introduced in C++11 that can capture external variables to form a closure (Closure). However, if used carelessly, it is easy to fall into some hidden traps, leading to undefined behavior, dangling references, performance issues, or even crashes.

Trap 1: Capturing local variables by reference, leading to dangling references

This is the most dangerous and common trap.

#include <functional>#include <iostream>std::function<int()> dangerous_lambda() {    int x = 10;    // Capture x by reference, but x is a local variable!    return [&x]() { return x; }; }int main() {    auto lambda = dangerous_lambda();    std::cout << lambda(); // ❌ Undefined behavior! x has been destructed}

The lambda captures the reference of the local variable <span>x</span>, but after the function returns, <span>x</span> is destroyed, making the reference inside the lambda a dangling pointer.The correct approach is:

  • Capture by value (if the variable is small and copyable)
  • or ensure the lifetime of the captured object is longer than the lambda
std::function<int()> safe_lambda() {    int x = 10;    return [x]() { return x; }; // ✅ Captured by value, safe}

Trap 2: Capturing this pointer when the object is destroyed

When creating a lambda in a class member function and using it asynchronously, the object may have been destroyed.

class MyClass {public:    void start_timer() {        std::thread([this]() {            std::this_thread::sleep_for(2s);            std::cout << value; // ❌ May access a destroyed object        }).detach();    }private:    int value = 42;};int main() {    {        MyClass obj;        obj.start_timer(); // Start thread    } // obj destructs, but the thread may still be running    std::this_thread::sleep_for(3s);}

Problem: The object pointed to by <span>this</span> <span>obj</span> has been destroyed in the main thread, but the background thread is still using it.

Use <span>shared_from_this</span> (must inherit from <span>std::enable_shared_from_this</span>)

class MyClass : public std::enable_shared_from_this<MyClass> {public:    void start_timer() {        auto self = shared_from_this(); // Increase reference count        std::thread([self]() {            std::this_thread::sleep_for(2s);            std::cout << self->value; // ✅ Safe, self ensures the object stays alive        }).detach();    }private:    int value = 42;};

Or pass the value instead of <span>this</span>

void start_timer() {    int val = value; // Copy value    std::thread([val]() {        std::this_thread::sleep_for(2s);        std::cout << val;    }).detach();}

Trap 3: Capturing loop variables by reference in a loop

Creating multiple lambdas in a loop, all capturing the loop variable by reference, can lead to unexpected results.

#include <vector>#include <functional>#include <iostream>std::vector<std::function<void()>> lambdas;for (int i = 0; i < 3; ++i) {    lambdas.push_back([&i]() {         std::cout << i << " ";     });}for (auto& f : lambdas) {    f(); // ❌ Output may be 3 3 3 (i has become 3)}

All lambdas reference the same variable <span>i</span>, and after the loop ends, <span>i == 3</span>, so they all output <span>3</span>.

Capture by value:

for (int i = 0; i < 3; ++i) {    lambdas.push_back([i]() { // Capture each i by value        std::cout << i << " ";    });}// Output: 0 1 2 ✅

Or create a new variable inside the loop

for (int i = 0; i < 3; ++i) {    const int index = i; // New local variable    lambdas.push_back([&index]() {         std::cout << index << " ";    });}

Trap 4: Implicit capture leading to unexpected behavior

Using <span>[=]</span> or <span>[&]</span> for implicit capture may capture unnecessary variables, leading to performance degradation or lifetime issues.

int a = 1, b = 2, c = 3;auto lambda = [&]() { // Implicitly captures all    return a + b; // Actually only needs a and b};

If <span>c</span> is a large object, although not used, it is also implicitly captured (by reference), which may affect code readability and safety.

It is recommended to capture explicitly

auto lambda = [&a, &b]() { // Explicitly state to only capture a and b    return a + b;};

Trap 5: Accessing by reference after moving an object

Captured a reference to an object, but that object was later moved by <span>std::move</span>.

std::string data = "Hello";auto lambda = [&data]() {     std::cout << data; // ❌ data may have been moved};std::string moved = std::move(data); // data enters a valid but undefined statelambda(); // Dangerous!

Solution: capture by value

auto lambda = [data = std::move(data)]() mutable { // Move capture (C++14)    std::cout << data;};

Trap 6: Lifecycle issues when returning a lambda

Returning a lambda that captures a local object.

auto create_adder(int x) {    return [&x](int y) { return x + y; }; // ❌ x is a local variable}

Similar to Trap 1, <span>x</span> is a function parameter and is destroyed after the function returns.

auto create_adder(int x) {    return [x](int y) { return x + y; }; // ✅ Captured by value}

Core Principle: The lambda captures a “snapshot” or “reference” of the variable, and it must ensure that this snapshot is valid throughout the entire lifecycle of the lambda.

Leave a Comment