Home | Projects | Notes > C++ Programming > Stateful Lambda Expressions
A stateful lambda expression in C++ is a lambda that captures variables from its surrounding scope, allowing it to retain state between calls or access external context. This is done through the capture list, the part in square brackets []
before the parameter list.
11[captured_variables] () -> return_type specifiers { };
captured_variables
- Non-empty capture list: Defines what information/variables should be captured.
Lambda definition:
11auto l = [] (int x) { std::cout << x; };
Compiler-generated closure:
71class CompilerGeneratedName
2{
3public:
4 CompilerGeneratedName(); // default constructor
5
6 void operator() (int x) { std::cout << x; } // overloaded operator() function
7};
A compiler-generated closure is an unnamed class automatically created by the compiler to represent a lambda expression in C++. It contains:
An overloaded
operator()
– defines the behavior (the lambda body).Captured variables as data members – if the lambda is stateful.
Possibly a default constructor and copy/move constructors.
The class behind a lambda expression isn't generated until compile time. This explains why the auto
keyword must be used when declaring a variable to hold a lambda. At the point we define the lambda, its type doesn't yet exist—it will only be created by the compiler during compilation. Since the type is unique and unnamed, only the compiler can know and assign it.
When we assign a lambda expression to a variable, we're actually instantiating an object of the compiler-generated closure class. This involves an implicit call to the constructor of that class. In essence, lambda expressions work behind the scenes by creating unnamed function objects at compile time, encapsulating both behavior and any captured state.
Lambda definition:
21int y {10};
2auto l = [y] (int x) { std::cout << x + y; };
Compiler-generated closure:
When a stateful lambda expression is instantiated, the compiler creates a unique function object using a parameterized constructor. This object stores the captured variables as member variables, allowing the lambda to retain context from its surrounding scope.
91class CompilerGeneratedName
2{
3private:
4 int y;
5public:
6 CompilerGeneratedName(int y) : y{y} { }; // parametrized constructor
7
8 void operator() (int x) const { std::cout << x + y; }
9};
This time, the overloaded operator()
function is a constant member function, meaning it cannot modify any member variables of the instantiated object. As a result, the member variable y
cannot be changed. This is because, by default, all variables captured by value in a lambda are captured as const. While this helps ensure safety, it can be limiting when we need to modify captured values. To address this, C++ provides several ways to capture variables with more flexibility.
Capture by value (default capturing mode):
21int x{100};
2[x] () { std::cout << x; }(); // Displays 100
Remember! A variable being captured by value is actually being captured by
const
value which won't be modifiable within the lambda.
Using mutable
to modify variables captured by value:
91int x{100};
2
3[x] () mutable
4{
5 x += 100;
6 std::cout << x; // Displays 200
7} ();
8
9std::cout << x; // Displays 100
L3: The keyword
mutable
is used to tell the compiler to generate the lambda's operator function as a non-const member function. This way the variable captured by value can be modified within the lambda. Remember! The captured variable is still a copy of the original variable passed to the lambda.
Capture by reference:
41int x{100};
2
3[&x] () { x += 100; }();
4std::cout << x; // Displays 200
Capture by value and reference:
41[x, y] // Capture both x and y by value
2[x, &y] // Capture x by value and y by reference
3[&x, y] // Capture x by reference and y by value
4[&x, &y] // Capture both x and y by reference
Default captures:
A default capture allows a lambda to capture "all" variables referenced within its body according to the defined capture mode.
31[=] // Default capture by value
2[&] // Default capture by reference
3[this] // Default capture this object by reference
L3: The keyword
this
indicates that all member variables of the current object, as referenced within the lambda, should be captured by reference.
Using default and explicit captures:
This approach allows you to mix default capture modes ([=]
or [&]
) with explicitly captured variables, giving you fine-grained control over how each variable is captured.
31[=, &x] // Default capture by value but capture x by reference
2[&, y] // Default capture by reference but capture y by value
3[this, z] // Default capture this by but capture z by value
The default capture must come first in the capture list. Also, the explicit capture cannot be the same as the default. Otherwise, the lambda won't compile.
With so many possible combinations of default and explicit captures, it's difficult to cover them all. However, what we've discussed should equip you for most situations where stateful lambdas are needed.