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.