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 CompilerGeneratedName2{3public:4    CompilerGeneratedName();    // default constructor5    6    void operator() (int x) { std::cout << x; } // overloaded operator() function7};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 CompilerGeneratedName2{3private:4    int y;5public:6    CompilerGeneratedName(int y) : y{y} { };    // parametrized constructor7    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 100Remember! A variable being captured by value is actually being captured by
constvalue which won't be modifiable within the lambda.
Using mutable to modify variables captured by value:
91int x{100};2
3[x] () mutable4{5    x += 100;6    std::cout << x;     // Displays 2007} ();8
9std::cout << x;         // Displays 100L3: The keyword
mutableis 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 200Capture by value and reference:
41[x, y]      // Capture both x and y by value2[x, &y]     // Capture x by value and y by reference3[&x, y]     // Capture x by reference and y by value4[&x, &y]    // Capture both x and y by referenceDefault 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 value2[&]         // Default capture by reference3[this]      // Default capture this object by referenceL3: The keyword
thisindicates 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 reference2[&, y]      // Default capture by reference but capture y by value3[this, z]   // Default capture this by but capture z by valueThe 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.