Mutable Lambda in C++
In this article, we will discuss the Mutable Lambda in C++ with its examples and several methods. But before discussing the mutable lambda, we must know about the lambda in C++.
What are lambdas?
A concise method for defining an anonymous function object (functor) in C++ is to use a lambda expression. A thing that can be called like a function is called a functor. Lambdas are functions that can capture variables from their enclosing scope; they are also called closures.
Purpose of lambdas:
Lambdas are helpful for many things, such as:
- Defining callback functions that are passed to other functions and algorithms.
- Putting in place unique filtering and sorting logic.
- Making handlers for events.
- Capturing state from the surrounding field of view.
Gathering variables from the surrounding area:
The ability of lambdas to capture variables from their enclosing scope is one of their primary characteristics. It implies that even after the lambdas have been returned from the function, they can still access and change the variables from the function or block in which they are defined.
The capture clause at the start of the lambda expression is used to capture variables from the enclosing scope. A list of variables enclosed in square brackets makes up the capture clause. An ampersand (&) before a variable name indicates that the variable will be captured by reference. If not, the variable's value will be used to capture it.
The variable x from the enclosing scope is captured by reference in the lambda expression that follows, for instance:
int x = 10; auto lambda = [&](int y) { x++; return x + y; }; int x = 10, y = 20; auto lambda = [x, y](int z) { return x + y + z; };
The variable x will be increased, and the sum of x and the argument y will be returned when the lambda expression is called.
Additionally, lambdas can store variables from various scopes and multiple variables at once. The variables x and y from the enclosing content are captured by value in the lambda expression that follows, for instance:
Lambda Syntax in C++:
It has the following syntax:
The basic syntax of a lambda function in C++ is as follows: [capture clause] (parameter list) -> return type { function body }
Capture clause:
A list of parameters that the lambda function may use from its enclosing scope is contained in the capture clause. Square brackets ([]) enclose the capture clause. An ampersand (&) before a variable name indicates that the variable will be captured by reference. If not, the variable's value will be used to capture it.
List of parameters:
The list of parameters that the lambda function takes is called the parameter list. Parentheses (()) enclose the parameter list. The compiler can infer the parameter types from the function body, or they can be explicitly specified.
int x = 10; auto lambda = [&](int y) { return x + y; };
Uses for the lambda function
There are several uses for the lambda function in C++. Some main uses of the lambda function are as follows:
More intricate syntax:
The basic syntax previously explained can be more straightforward than the lambda syntax. For instance, lambda functions can be generic and have many capture clauses. But a solid place to start when learning about lambda functions is with the fundamental syntax that was previously mentioned.
Lambda Capture Clauses:
Lambda expressions, variables can be captured in three different ways:
- Value capture
- Reference capture
- Mixed capture
- Value capture
The standard capture mode is value capture. Copies of the variables are captured from the enclosing scope by the lambda expression during value capture. It indicates that the original variables in the enclosing area are not modifiable by the lambda expression, it can access and alter the copies of the variables.
The following capture clauses can be used to capture variables based on their value:
- All variables from the enclosing scope are captured by weight using the [=] capture clause.
- [x, y] - The variables x and y are taken by value from the enclosing scope by this capture clause.
Reference Capturing:
The lambda expression can access and change the original variables in the enclosing scope due to reference capture. The following capture clauses can be used to capture variables by reference:
- [&] - Every variable in the enclosing scope is captured by reference in this capture clause.
- [&x, &y] - The variables x and y are reference-captured from the enclosing content by this capture clause.
We can capture some variables by value and others by reference using mixed capture. The following capture clauses can be used to capture variables using hybrid capture:
- [=, &x] - This capture clause uses a reference to capture the variable x, but all other variables from the enclosing scope are captured by value.
- [&, x] - This capture clause uses a reference to capture all variables from the enclosing scope; the variable x is captured by value.
How do Mutable Lambdas work?
In C++, mutable lambdas are a unique kind of lambda expression that lets the expression change the variables it obtains from its enclosing scope. Lambda expressions cannot change the captured variables by default because they are immutable. It is because callback functions, which usually use lambda expressions, must ensure they do not change the state of the enclosing scope.
Nonetheless, there are situations in which changing the captured variables using a lambda expression is required. For instance, a lambda expression must change the captured variable holding the count to implement a counter.
Why Do We Need Mutable Lambdas?
Mutable lambdas are required because there are situations in which a lambda expression must change the captured variables. Mutable lambdas may cause side effects and complicate code interpretation, so it's crucial to use them carefully.
Using Mutable Lambdas:
The mutable keyword must be used in the capture clause to use a mutable lambda. For instance, the lambda expression that follows extracts the variable x by reference from its surrounding scope and modifies it:
int x = 10; auto lambda = [&mutable x](int y) { x++; return x + y; };
The variable x will be increased by this lambda expression, which will also return the sum of x and its argument.
Syntax of Mutable Lambdas:
Mutable lambdas are a unique kind of lambda expression that lets the expression change the variables it obtains from its enclosing scope. Lambda expressions cannot change the captured variables by default because they are immutable. It is because callback functions, which usually use lambda expressions, must ensure they do not change the state of the enclosing scope.
Adding the Mutable Keyword:
The mutable keyword must be included in the capture clause to create a mutable lambda. The capture list should come right after the mutable keyword. For instance, the lambda expression that follows references the variable x to be captured from its surrounding scope and modifies it.
int x = 10; auto lambda = [&mutable x](int y) { x++; return x + y; };
The variable x will be increased by this lambda expression, which will also return the sum of x and its argument.
Placing the Mutable Keyword:
The mutable keyword can appear anywhere in the capture clause if it comes right after a capture list. For instance, the previous lambda expression can also be expressed as follows:
int x = 10; auto lambda = [mutable &x](int y) { x++; return x + y; };
This lambda x will also increase the variable expression, which will return the sum of x and its argument.
Use Cases of Mutable Lambdas:
When a lambda expression needs to change the captured variables, mutable lambdas are a helpful tool for creating clear, concise code. Here are some specific instances of situations where mutable lambdas come in handy:
Modifying a Variable Captured by Value:
Value-captured variables can be modified with mutable lambdas. For example, let's say you want to write a lambda expression that increases a counter variable in the following scenario:
#include <iostream> int main() { int counter = 0; // Create a lambda expression to increment the counter auto incrementCounter = [=]() mutable { counter++; }; incrementCounter(); std::cout << "Counter: " << counter << std::endl; return 0; }
Output:
Explanation:
In this example, the counter variable is captured by value, but the lambda expression can change it due to the mutable keyword. The counter variable can be increased by repeatedly calling the incrementCounter lambda expression.
Implementing Custom Sort in Place:
Custom sorting logic can be implemented with mutable lambdas. For instance, think about the following situation where you wish to order a vector of strings according to their length:
vector strings{"apple", "orange", "banana"}; sort(strings.begin(), strings.end(), [](const string& s1, const string& s2) { return s1.length() < s2.length(); });
Here, the lambda expression determines whether the first string is shorter than the second by comparing their lengths and returning true. It enables you to use the sort algorithm to sort the string vector according to size.
Putting an Event Handler in Place:
Event handlers can be implemented using mutable lambdas. For illustration, let's say you want to write a lambda expression that, upon button click, increases a counter variable in the following scenario:
button.onClicked.connect([]() mutable { counter++; });
In this instance, the button's onClicked event is linked to the lambda expression. The lambda expression will increase the counter variable each time the button is pressed.
Capture Defaults in Lambda Expressions-
Anonymous functions can be concisely defined using lambda expressions. They can capture them from the area they are enclosing to access and alter variables. Although lambda expressions provide flexibility, there is a lengthy and repetitive nature to capturing variables. You can make this process simpler by defining the default capture mode for variables using capture defaults.
Default Captures for Modifiable Lambdas:
Capture defaults can expedite the capture process for mutable lambdas, which lets the lambda expression to change captured variables. Lambda expressions capture variables by value by default, which means they capture a copy of the variable's value.
Use the & capture default to store variables by reference. It enables the original variable in the enclosing scope to be directly modified by the lambda expression. It is often the desired behaviour for mutable lambdas.
Examples of Capture Defaults:
Take a look at the mutable lambda expression example that increases a counter variable below:
int counter = 0; auto incrementCounter = [&]() { counter++; };
This lambda expression enables it to alter the original variable by capturing the counter variable by reference. The & capture default streamlines the capture procedure by avoiding the need to explicitly specify the capture mode.
Another illustration shows how to use capture defaults to capture multiple variables:
int x = 10, y = 20; auto lambda = [&x, y]() { x++; y++; };
By capturing x and y by reference, this lambda expression can change the initial variables. Both variables receive the & capture default, which streamlines the capture procedure.
Mixing Capture Defaults and Explicit Captures:
You can combine explicit captures with capture defaults to capture variables based on value or reference. Take the following example, for instance:
int x = 10, y = 20; auto lambda = [&x, =y]() { x++; };
Y is captured by value (=) and x by reference (&) in this lambda expression. For x, the explicit capture mode is used, and for y, the capture default. It enables you to record variables in various ways according to your needs.
Use Cases:
In many real-world scenarios, mutable lambdas are helpful, especially when working with customized sorting algorithms or altering data. Here are a few particular instances:
1. Modifying Elements in a Container:
Modifying elements within a container can be done quickly and effectively with mutable lambdas. Let's say you wish to increase the value of each component of an array.
int arr[] = {1, 2, 3, 4, 5}; for (int& element : arr) { element++; }
This code iterates through the array, changing each element using a mutable lambda expression. The lambda can alter the array's original element directly because the element variable is captured by reference.
2. Implementing Custom Comparators:
You can write custom comparators for sorting algorithms using mutable lambdas. For example, let's say you wish to reverse the order in which a list of strings is sorted according to their length:
#include <iostream> #include <vector> #include <algorithm> int main() { std::vector<std::string> strings{"apple", "orange", "banana","grapes"}; // Sort the strings based on their length std::sort(strings.begin(), strings.end(), [](const std::string& s1, const std::string& s2) { return s1.length() > s2.length(); }); // Print the sorted strings for (const std::string& str : strings) { std::cout << str << std::endl; } return 0; }
Output:
Explanation:
This code compares the lengths of two strings using a mutable lambda expression. The sorting order is determined by modifying a temporary variable called temp using the lambda expression.
3. Updating State Information:
Within a program, mutable lambdas can be used to update state data. Imagine the following situation where you would like to keep track of how many times a button has been clicked:
#include <iostream> #include <functional> class Button { public: std::function<void()> onClicked; void click() { if (onClicked) { onClicked(); } } }; int main() { int clickCount = 0; Button button; button.onClicked = [&clickCount]() mutable { clickCount++; }; button.click(); std::cout << "Click Count: " << clickCount << std::endl; return 0; }
Output:
Explanation:
This code associates a mutable lambda expression with a button's onClicked event. The lambda expression increases the clickCount variable when the button is pressed, recording the total number of clicks.
4. In-Place Algorithms:
Implementing in-place algorithms with mutable lambdas allows you to change the data without making temporary copies. Take an array's in-place reversal as an illustration
#include <iostream> #include <algorithm> int main() { int arr[] = {1, 2, 3, 4, 5, 6}; std::for_each(arr, arr + 6, [](int& element) { element = -element; // Negate each element to effectively reverse them }); for (int i : arr) { std::cout << i << " "; } return 0; }
Output:
This code reverses the elements of the array by using a mutable lambda expression to swap the elements in place.