Functions are a powerful tool and usually work well. Sometimes, though, we need simple, one-time functions that we can express in just a few lines of code. Regular functions might not be the best choice in these scenarios. That's where lambda functions, also known as anonymous functions, come in. These inline functions are great for short-term tasks like function arguments or creating simple event handlers. They're a helpful tool that we can easily integrate into our code.
Getting started
Let's start with a greeting using the common function approach:
#include <iostream>
void greet() {
std::cout << "Hello world!" << std::endl;
}
int main() {
greet();
return 0;
}Now, let's try using lambda functions for the same purpose. First, we will look at the syntax of lambdas in C++.
[capture] (parameters_list) -> return_type {
// Statements
}capture: Capture variables from the surrounding scope. You can capture these variables either by value or by reference for use within the lambda scope.
parameters_list: A list of parameters, like in a normal function definition. It can be empty if the function takes no arguments.
return_type: The function's return type (optional).
Statements: The code executed when the lambda expression is called.
As you can see, we don't need a name to define this type of function; that's why they are also known as anonymous functions. In these functions, the capture, parameters, return type, and even the statements are optional. The previous example, rewritten using lambda functions, will look like this:
#include <iostream>
int main() {
[]() {
std::cout << "Hello world!" << std::endl;
}();
return 0;
}In the code above, the lambda function is defined within another function, specifically the main function. Moreover, the lambda is executed immediately after its definition, which the use of () following the lambda's body indicates.
Storing lambdas in variables
Suppose we need to take a number from the user and multiply it by 10. Using the normal functions approach, the program would look like this:
#include <iostream>
int multiplyByTen(int x) {
return x * 10;
}
int main()
{
int number {};
int result {};
std::cout << "Please type a number: ";
std::cin >> number;
result = multiplyByTen(number);
std::cout << "The result is: " << result << std::endl;
return 0;
}Please type a number: 11
The result is: 110One convenient feature of lambdas is that we can store them in variables, which lets us reuse and call them multiple times in our code. It looks like this:
auto variable_name {[capture] (parameters_list) -> return_type {
// Statements
}};An important detail here is the lambda function's type. We use auto to let the compiler deduce the lambda's type. That's because each lambda has a unique type generated at compile time that cannot be explicitly named in C++ code.
Let's rewrite the example using lambda functions:
#include <iostream>
int main()
{
int number {};
int result {};
std::cout << "Please type a number: ";
std::cin >> number;
// Defining the lambda function
auto multiplyByTen {[](int x) {
return x * 10;
}};
// Using the lambda function
result = multiplyByTen(number);
std::cout << "The result is: " << result << std::endl;
return 0;
}Here is the output for the number 11:
Please type a number: 11
The result is: 110 The code we write is equivalent to one that uses a normal function. By storing our lambda in the multiplyByTen variable, we can refer to our lambda function and use it whenever we want.
Capturing variables by value
One of the features of lambdas is their ability to capture variables from their surrounding scope. Imagine we want to receive a word from the user and compare it against the word "Hi". We could implement something like the following code:
#include <iostream>
int main()
{
std::string word {};
std::cout << "Please type a word: ";
std::cin >> word;
// Defining the lambda function
auto compare { [](std::string other_word) {
return word == other_word; // Error word is not accessible in lambda scope
}};
// Using the lambda function
// Compare against "Hi"
if (compare("Hi")) {
std::cout << "Congrats! Your word is Hi" << std::endl;
} else {
std::cout << "Bad luck, try again" << std::endl;
}
return 0;
}All seems good in the above code since the lambda function is defined inside the main function's scope. It would make sense that we can use the word variable and directly proceed to compare. However, if you try to compile this code, you will end up with a compilation error. This happens because lambdas don't have direct access to variables outside their own scope.
This is where the capture clause comes to the rescue. It allows lambdas to access variables from their surrounding scope. When capturing by value, it creates a const copy of each captured variable, making them read-only inside the lambda and preventing modifications.
Let's make the above code work. We will capture the word variable so we can use it in the scope of our lambda, as follows:
// Defining the lambda function
auto compare { [word](std::string other_word) {
return word == other_word;
}}; Now we are capturing the word variable from the main() scope, and we can use it for comparison inside our lambda without getting errors. For example, if you test the program with the word "Hello", you will receive the following output:
Please type a word: Hello
Bad luck, try againJust remember, when we capture by value, a const copy is created for the captured variables. So bear in mind that you can't modify the variables inside the lambda's scope.
Capturing variables by reference
We know how to capture variables by value. However, we must be aware that we cannot modify the captured variables, as they are read-only. To solve this problem, lambdas also allow us to capture by reference.
Let's demonstrate the disadvantage of capturing by value. Suppose we want to count how many times a lambda function is called using captures. Here's what the implementation would look like when capturing by value:
#include <iostream>
int main()
{
int count {};
auto counter {[count]() {
++count; // Error count is read-only
std::cout << "Count: " << count << std::endl;
}};
// Call counter three times
for (int i = 0; i < 3; ++i) {
counter();
}
std::cout << "Final count: " << count << std::endl;
return 0;
}To solve this issue, we can capture variables by reference. The syntax is similar to capturing by value, but you precede the variable in the capture with an '&'. With this type of capture, you have a non-const reference that you can use inside our lambda scope. Therefore, if we modify this reference, we also modify the actual variable.
We can change our lambda definition using this type of capture as follows:
auto counter {[&count]() { // Capturing by reference
++count;
std::cout << "Count: " << count << std::endl;
}};The code output using the above lambda:
Count: 1
Count: 2
Count: 3
Final count: 3As you can see, we count how many times the function was called, and the final state of the count variable was 3. We achieved this thanks to capturing by reference.
Capturing variables by reference in lambda functions can lead to dangling references if the captured variables go out of scope before the lambda is executed. This can cause undefined behavior. To prevent this, ensure the captured variables outlive the lambda or capture by value.
Here's an example:
#include <iostream>
auto createLambda() {
int x = 10;
return [&]() { std::cout << x << std::endl; }; // Dangling reference
}
int main()
{
auto myLambda = createLambda();
myLambda(); // Undefined behavior
return 0;
}This is a case of a dangling reference because the variable x goes out of scope once createLambda() finishes execution, leaving the lambda's reference to x invalid. When the lambda is invoked in main() by calling myLambda(), it attempts to access the non-existent x, leading to undefined behavior.
Capturing multiple variables
We can also capture multiple variables by value. In this case, we must use [=], which captures all variables from the surrounding scope by value. Let's write a program that takes two words from the user and creates a sentence:
#include <iostream>
int main()
{
std::string firstWord {};
std::string secondWord {};
// Getting the two words from the user
std::cout << "Type the first word: ";
std::cin >> firstWord;
std::cout << "Type the second word: ";
std::cin >> secondWord;
auto writeSentence{[=]() { // Capture all variables by value
std::cout << firstWord << " " << secondWord << std::endl;
}};
writeSentence();
return 0;
}If we test the above code with the words 'Hello' and 'World!', we will get the following result:
Type the first word: Hello
Type the second word: World!
Hello World!We can also conveniently capture all the variables in the surrounding scope by reference using the [&] capture. Imagine we want to use the capture to change the values of two numbers. For this scenario, we could do something like this:
#include <iostream>
int main()
{
int firstNumber {1};
int secondNumber {2};
// Lambda to change the numbers' values
auto changeValues {[&](int x, int y) {
firstNumber = x;
secondNumber = y;
}};
std::cout << "First number before: " << firstNumber << std::endl;
std::cout << "Second number before: " << secondNumber << std::endl;
changeValues(2, 4);
std::cout << "First number after: " << firstNumber << std::endl;
std::cout << "Second number after: " << secondNumber << std::endl;
return 0;
}That will have the following output:
First number before: 1
Second number before: 2
First number after: 2
Second number after: 4When using the [&] capture in a lambda, it allows the lambda to access variables from its immediate scope as well as from any enclosing scopes, such as the surrounding function or loops. For example:
#include <iostream>
#include <vector>
int main()
{
int outerVariable = 10;
std::vector<int> numbers = {1, 2, 3, 4, 5};
for (int i = 0; i < numbers.size(); ++i) {
int innerVariable = 20;
auto lambda = [&]() {
std::cout << "Outer variable: " << outerVariable << std::endl;
std::cout << "Inner variable: " << innerVariable << std::endl;
std::cout << "Current number: " << numbers[i] << std::endl;
};
lambda();
}
return 0;
}In this example, the lambda function captures all variables by reference using [&] and can access outerVariable from the function's scope, innerVariable from the loop's scope, and numbers[i] from the loop's scope.
To capture all surrounding variables, use [&] for reference and [=] for value.
Initializing Capture (C++14)
C++14 introduced an initializing capture, which allows the creation and initialization of captured variables directly within a lambda. This feature improves code readability and efficiency by removing the need for outside variable declarations.
Here's how to use an initializing capture:
#include <iostream>
int main()
{
int x = 10;
// Initializing capture in C++14
auto lambda = [y = x + 1]() {
std::cout << "Value of y: " << y << std::endl;
};
lambda(); // Output: Value of y: 11
return 0;
}In this example, y is captured and initialized with the value of x + 1 right within the lambda expression.
Universal Lambdas (C++20)
Starting with C++20, you can use auto in the lambda parameter list to create universal lambdas. This allows the lambda to accept any type of argument.
#include <iostream>
int main()
{
auto add = [](auto x, auto y) {
return x + y;
};
std::cout << add(5, 10) << std::endl; // Output: 15
std::cout << add(2.5, 3.0) << std::endl; // Output: 5.5
return 0;
}In this example, the lambda function accepts arguments of any type compatible with the + operator (such as int and double) because of the use of auto in the parameter list. This feature provides flexibility and convenience when working with lambda functions.
Using Lambdas with Standard Library Algorithms
Lambdas are commonly used with algorithms from the C++ standard library, such as std::sort, std::for_each, and std::transform. These algorithms often require a predicate or callback function, which can be conveniently expressed using lambdas. For example:
#include <iostream>
#include <algorithm>
#include <vector>
int main()
{
std::vector<int> numbers = {3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5};
// Sorting the vector with std::sort
std::sort(numbers.begin(), numbers.end(), [](int a, int b) {
return a < b;
});
// Divide each element by 2 using std::transform
std::vector<int> transformed_numbers;
std::transform(numbers.begin(), numbers.end(), std::back_inserter(transformed_numbers), [](int x) {
return x / 2;
});
// Using for_each to print each element using std::for_each
std::cout << "Sorted and transformed vector: ";
std::for_each(transformed_numbers.begin(), transformed_numbers.end(), [](int x) {
std::cout << x << " ";
});
std::cout << std::endl;
return 0;
}Sorted and transformed vector: 0 0 1 1 1 2 2 2 2 3 4 In this code, the lambda [](int a, int b) { return a < b; } is used as a predicate for std::sort to sort the numbers vector in ascending order. The lambda [](int x) { return x / 2; } is used as a predicate for std::transform to transform the numbers. Subsequently, the lambda [](int x) { std::cout << x << " "; } is used with std::for_each to print each element of the sorted and transformed vector.
Conclusion
Lambda functions in C++ offer a flexible and potent way to define anonymous functions right where we need them. It's convenient to store lambdas in variables so we can use them more than once, which improves code reusability and maintenance. They let us capture variables by value or reference, giving us the flexibility to choose how we interact with them inside the function. Knowing how to capture variables is crucial for effective lambda use. Moreover, features like initializer captures and universal lambdas, along with STL algorithms, greatly boost the capabilities of lambdas. In sum, lambda functions enable us to write code that's both cleaner and more efficient.