Hello! In our previous lessons, we've built a solid foundation in modern C++ resource management, culminating in understanding how to use std::move to explicitly transfer ownership and avoid costly copies. We've focused on efficiently managing data. Now, we'll turn our attention to managing logic.
This lesson introduces lambda expressions, a powerful feature for creating anonymous, inline functions. Our focus will be on making these functions stateful—that is, capable of remembering information between calls. This skill is essential for writing clean, efficient code for tasks like handling user input in your whiteboard application or implementing custom logic for C++ standard library algorithms. You will learn how to write lambdas that can maintain their own internal state, modify external state, and even take ownership of resources.
What is a Lambda Expression?
At its simplest, a lambda expression lets you define a function right where you need it, without giving it a name. This is incredibly useful for short, single-use functions passed to algorithms or used as callbacks. You've likely seen the basic syntax before: [] () {}.
Behind this concise syntax, the compiler performs a clever transformation. A lambda expression is essentially syntactic sugar for creating an unnamed class type, called the closure type, and instantiating an object of that type, the closure object. This object has its operator() overloaded with the code you provided in the lambda's body, making the object callable like a function.
This "function object" nature is what gives lambdas their power and performance. Since they are objects, they can have member variables, which is how they store state. The following diagram breaks down the full syntax of a lambda expression.

We will focus on the capture clause [ ] and the mutable specifier, as these are the keys to creating stateful lambdas.
Capturing the Environment: A Lambda's State
The real power of lambdas comes from the capture clause []. This allows the lambda to access—or "capture"—variables from its surrounding scope. These captured variables become, in effect, data members of the closure object, giving the lambda its state.
There are two primary ways to capture variables:
- Capture by Value (
[=]or[x]): The closure object stores a copy of the captured variable. The lambda operates on its own private copy, and any changes it makes will not affect the original variable. - Capture by Reference (
[&]or[&x]): The closure object stores a reference to the original variable. Any changes the lambda makes will directly affect the original variable.
To see what this means in practice, it's helpful to see how the compiler translates lambdas. The following resource provides examples of what the generated closure class looks like.
C++ Lambda Code Generation with Init Captures in C++ 14
This Stack Overflow answer uses a tool called CppInsights to show the compiler-generated code for lambdas. It provides a clear, low-level view that aligns with an interest in systems programming.
Focus on the generated code for Case 1, capture by value, and Case 2, capture by reference. Notice how in the first case, the closure class __lambda_5_16 has an int x; member, while in the second case, it has an int & x; member. This distinction is fundamental.
Capturing by reference is powerful, but it comes with a significant responsibility: you must ensure the reference does not outlive the object it refers to. If a lambda captures a local variable by reference and is then returned or stored to be used later (e.g., as a callback), you risk creating a dangling reference when the original variable goes out of scope. Capturing by value is always safe in this regard.
Modifying State: The mutable Keyword
By default, a lambda's call operator (operator()) is const. This means that even if you capture a variable by value, you cannot modify that copy inside the lambda. This is a safety feature to prevent accidental state changes, as lambdas are often expected to be pure, stateless predicates.
int counter = 0;
auto my_lambda = [counter]() {
// counter++; // COMPILE ERROR: cannot assign to a member of a closure object
std::cout << counter << std::endl;
};
To modify a variable captured by value, you must mark the lambda as mutable. This removes the const qualifier from the operator(), allowing the lambda to modify its own internal state.
auto stateful_counter = [count = 0] () mutable {
count++;
std::cout << "Lambda has been called " << count << " times." << std::endl;
};
stateful_counter(); // Prints "Lambda has been called 1 times."
stateful_counter(); // Prints "Lambda has been called 2 times."
Here, count is a data member of the closure object, initialized to 0. Each call to stateful_counter() modifies this internal state. This creates a stateful function object without polluting the surrounding scope with the count variable.
The following resource provides a clear explanation of captures and the mutable keyword.
Lambdas: From C++11 to C++20, Part 1
This article from C++ Stories offers a great step-by-step guide to lambda features. It clearly explains the mutable keyword in the context of captures.
Read the sections titled "The Call Operator" and "Captures". Pay special attention to the subsection "Mutable", which provides a concrete example of modifying a captured-by-value variable.
Stateful Lambdas for Custom Algorithm Logic
One of the most common uses for lambdas is to provide custom logic to the C++ Standard Library algorithms. A stateful lambda can perform complex filtering that depends on the sequence of elements.
For example, imagine you have a stream of user input points for a stroke and you want to thin them out, keeping only every Nth point. A stateful lambda is perfect for this.
#include <iostream>
#include <vector>
#include <algorithm>
struct Point { int x, y; };
int main() {
std::vector<Point> points = {{1,1}, {2,3}, {4,5}, {5,5}, {7,8}, {8,10}, {10,12}};
int n = 3;
// Use remove_if with a stateful lambda to keep every 3rd point
auto new_end = std::remove_if(points.begin(), points.end(),
[n, count = 0] (const Point&) mutable {
count++;
return (count % n) != 0; // Remove if not the Nth element
}
);
points.erase(new_end, points.end());
for (const auto& p : points) {
std::cout << "{" << p.x << ", " << p.y << "} ";
}
// Output: {4, 5} {10, 12}
// Note: The first element kept is at index 2 (count=3), then index 5 (count=6).
// The logic keeps the element when (count % n) == 0. The lambda returns true for elements to be *removed*.
return 0;
}
In this example, the lambda maintains a count variable. std::remove_if calls the lambda for each element, and our lambda decides whether to "remove" it based on its internal state.
The video below demonstrates a similar use case with std::find_if, using a lambda to define the search criteria inline.
This video from The Cherno provides a concise and practical look at lambdas. This specific segment shows how to use a lambda with a standard algorithm.
Watch the section where the presenter uses a lambda with std::find_if. This happens from the start of the example. It's a classic demonstration of providing custom, inline logic to an algorithm.
C++14 Init-Capture: The Ultimate State Control
C++14 introduced a major enhancement called init-capture (or generalized lambda capture). This feature allows you to declare and initialize a new variable directly within the capture clause. The syntax is [name = expression].
This has two profound implications for performance and design:
1. Moving Resources into Lambdas:
In our last lesson, we saw how std::move transfers ownership. What if you want a lambda to take ownership of a resource, like a std::unique_ptr? With C++11 captures, this was impossible because unique_ptr is move-only and cannot be copied.
Init-capture solves this elegantly. It provides the syntax to move the resource directly into a member of the closure object.
auto ptr = std::make_unique<MyHeavyObject>();
// The lambda takes ownership of the pointer. The original `ptr` is now null.
auto task = [p = std::move(ptr)]() {
p->do_something();
};
// Now 'task' can be executed later, e.g., on another thread,
// and it has exclusive ownership of the MyHeavyObject instance.
This pattern is critical for high-performance asynchronous programming, where you want to send self-contained work packages to other threads without incurring copy overhead or managing complex shared lifetimes.
2. Creating Arbitrary State:
Init-capture also lets you create state variables for the lambda that don't even exist in the outer scope, or whose type is different. We already saw this with [count = 0]. Another use is to pre-calculate an expensive value once, when the lambda is created, rather than in every call.
// Expensive calculation is done ONCE during lambda creation
auto optimized_check = [threshold = calculate_expensive_threshold()] (int value) {
return value > threshold;
};
These features make lambdas not just convenient, but a first-class tool for building efficient, encapsulated components.
Lambdas: From C++11 to C++20, Part 1
The same C++ Stories article has an excellent section on C++14 improvements.
Focus on the section "Captures With an Initializer". The examples for moving an object and for optimization are directly relevant to your goal of writing high-performance code.
For a definitive, low-level explanation of how captures work—especially init-captures with std::move—the following resource is invaluable.
C++ Lambda Code Generation with Init Captures in C++ 14
We return to the Stack Overflow answer to see how init-capture is handled by the compiler.
Review Case 4, which shows the generated code for a lambda that moves a unique_ptr. Then, read the concise summary in the <span data-type="resource_reading_textrange" data-resource-subitem-id="309d1eb6" data-range-start="The constructor will look like this:" data-range-end="into closures").">next answer and the detailed explanation from the C++ standard in the third answer. This confirms that the language is designed to be as efficient as possible, performing only the necessary move operation.
Conclusion
In this lesson, you've moved beyond simple, stateless lambdas and learned how to imbue them with state. This transforms them from simple inline functions into powerful, self-contained function objects capable of complex behavior. For your whiteboard project, stateful lambdas will be an indispensable tool for everything from handling sequences of user input events to implementing custom rendering logic.
Key Takeaways:
- Lambdas as Objects: A lambda is syntactic sugar for a compiler-generated class (the closure type). Captures become the data members of this class.
- Capture by Value vs. Reference: Capturing by value gives the lambda a private, safe copy of the state. Capturing by reference allows the lambda to modify external state but requires careful lifetime management.
- Stateful Modification with
mutable: To modify variables captured by value, you must mark the lambdamutable, which makes its call operator non-const. - Init-Capture for Performance: C++14's init-capture
[name = expression]is a powerful feature that allows you to move resources into a lambda, avoiding copies and enabling efficient asynchronous patterns. It also allows for creating arbitrary state within the lambda itself.
Having mastered tools for managing both data (std::move) and logic (stateful lambdas), we are now well-equipped to write sophisticated, high-performance C++ code. In our next lesson, we will apply this knowledge by analyzing the core data structures you'll use to store application data. We will compare the performance characteristics and memory layouts of std::vector, std::list, and std::unordered_map to understand how to make the right choice for the right job.