Lambda expressions & closures
Scenario
You need to pass a callback function to a UI button click handler. The callback needs to access a local `userId` variable, but a standard C++ function pointer cannot hold external state.
How do you pass a function that 'remembers' the local variables from where it was created?
Why it exists
Before C++11, if you wanted to pass a stateful callback to a function (e.g. an event handler that needs access to a local variable), you had to write a verbose custom 'functor' class with member variables and an overloaded `operator()`. Lambdas were introduced to eliminate this boilerplate, allowing you to define the function and its state inline.
Mental model
A lambda is not magic. Under the hood, the compiler literally writes a custom `class` for you, puts your captured variables in it as member variables, and overloads `operator()` to run your code. A 'Closure' is simply an instance of that invisible class.
Lambdas allow you to write inline, anonymous functions. Their superpower is the Capture Clause `[]`, which allows them to capture local variables from the surrounding scope by value or by reference. This allows them to act as closures (functions bound to state) without the boilerplate of writing custom structs.
Explanation
**The Capture Clause `[]`:**
- `[]`: Captures nothing (can be converted to a raw function pointer).
- `[x, &y]`: Captures `x` by value (makes a copy) and `y` by reference.
- `[=]`: Implicitly captures everything used in the body by value.
- `[&]`: Implicitly captures everything used in the body by reference.
**Mutable Lambdas:**
By default, variables captured by value (`[x]`) are `const` inside the lambda body. You cannot modify them. If you need to modify the copied state across multiple calls (e.g., a stateful counter), you must add the `mutable` keyword: `[x]() mutable { x++; }`.
**Real-world Engineering:**
Lambdas are the backbone of modern C++ multithreading (passing tasks to threads or thread pools), UI programming (event callbacks), and functional STL algorithms. Be extremely careful with `[&]` captures in asynchronous code: if the lambda outlives the scope where it was created, the references will dangle and crash the program. When passing a lambda to a thread, always capture by value (`[=]`) or explicitly capture specific variables by value.
Code examples
Stateful Callbacks
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
int threshold = 10;
std::vector<int> numbers = {5, 15, 8, 20};
// Captures 'threshold' by value.
auto is_above_threshold = [threshold](int n) {
return n > threshold;
};
int count = std::count_if(numbers.begin(), numbers.end(), is_above_threshold);
std::cout << "Count: " << count << '\n'; // Prints 2
// Stateful lambda using 'mutable'
int counter = 0;
auto generate_id = [counter]() mutable {
return ++counter;
};
std::cout << generate_id(); // Prints 1
std::cout << generate_id(); // Prints 2
return 0;
}
The first lambda safely captures the threshold by value. The second lambda modifies its own internal copy of `counter` across successive calls.
Key points
- Lambdas are just structs: A closure is simply an instance of an invisible struct with an overloaded `operator()`.
- Beware dangling references: Never capture by reference `[&]` if the lambda will outlive the current scope.
Common mistakes
- Dangling reference captures: If a lambda captures a local variable by reference (`[&x]`), and that lambda is stored (e.g., returned from a function or passed to a detached thread), the local variable will be destroyed when the scope ends, leaving the lambda with a dangling reference. Use `[=]` for async tasks.
Recall questions
- What is the difference between `[=]` and `[&]` in a lambda capture clause?
- Why would you need the `mutable` keyword in a lambda?
- How does the C++ compiler implement lambdas under the hood?
Questions & answers
A junior engineer passes a lambda to a new background thread: `std::thread t([&]() { doWork(local_buffer); }); t.detach();`. What is the critical bug here?
The lambda captures `local_buffer` by reference using `[&]`. Because the thread is detached, the current function will likely finish and destroy `local_buffer` before the thread finishes executing, causing a Use-After-Free crash. They should have captured by value `[=]` or `[local_buffer]`.
Approach: Recognize the intersection of reference lifetimes and asynchronous execution.
A developer writes a lambda `[=](){ return counter++; };` but the compiler throws an error saying `counter` is read-only. Why does this happen and how do you fix it?
When a lambda captures by value (like `[=]`), the generated `operator()` is marked `const` by default, meaning you cannot modify the copied state. You fix it by adding the `mutable` keyword: `[=]() mutable { return counter++; };`
Approach: Recall the strict `const` defaults of value-captured closures.