We are going to write a little program in which we play with lambda expressions in order to get a feeling for them:
- Lambda expressions do not need any library support, but we are going to write messages to the terminal and use strings, so we need the headers for this:
#include <iostream>
#include <string>
- Everything happens in the main function this time. We define two function objects that take no parameters and return integer constants with the values, 1 and 2. Note that the return statement is surrounded by curly brackets {}, like it is in normal functions, and the () parentheses, which denote a parameterless function, are optional, we don't provide them in the second lambda expression. But the [] brackets have to be there:
int main()
{
auto just_one ( [](){ return 1; } );
auto just_two ( [] { return 2; } );
- Now, we can call both the function objects just by writing the names of the variables they are saved to and appending the parentheses. In this single line, they are indistinguishable from normal functions for the reader:
std::cout << just_one() << ", " << just_two() << 'n';
- Now let's forget about those and define another function object, which is called plus because it takes two parameters and returns their sum:
auto plus ( [](auto l, auto r) { return l + r; } );
- This is also easy to use, just like any other binary function. As we defined its parameters to be of the auto type, it will work with anything that defines the plus operator +, just as strings do:
std::cout << plus(1, 2) << 'n';
std::cout << plus(std::string{"a"}, "b") << 'n';
- We do not need to store a lambda expression in a variable in order to use it. We can also define it in place and then write the parameters in parentheses just behind it (1, 2):
std::cout
<< [](auto l, auto r){ return l + r; }(1, 2)
<< 'n';
- Next, we will define a closure that carries an integer counter value around with it. Whenever we call it, it increments its counter value and returns the new value. In order to tell it that it has an internal counter variable, we write count = 0 within the brackets to tell it that there is a variable count initialized to the integer value 0. In order to allow it to modify its own captured variables, we use the mutable keyword, as the compiler would not allow it otherwise:
auto counter (
[count = 0] () mutable { return ++count; }
);
- Now, let's call the function object five times and print the values it returns, so we can see the increasing number values later:
for (size_t i {0}; i < 5; ++i) {
std::cout << counter() << ", ";
}
std::cout << 'n';
- We can also take existing variables and capture them by reference instead of giving a closure its own value copy. This way, the captured variable can be incremented by the closure, but it is still accessible outside. In order to do so, we write &a between the brackets, where the & means that we store only a reference to the variable, not a copy:
int a {0};
auto incrementer ( [&a] { ++a; } );
- If this works, then we should be able to call this function object multiple times, and then observe that it has really changed the value of variable a:
incrementer();
incrementer();
incrementer();
std::cout
<< "Value of 'a' after 3 incrementer() calls: "
<< a << 'n';
- The last example is currying. Currying means that we take a function that can accept some parameters and store it in another function object, which accepts fewer parameters. In this case, we store the plus function and only accept one parameter, which we forward to the plus function. The other parameter is the value 10, which we save in the function object. This way, we get a function, which we call plus_ten because it can add that value to the single parameter it accepts:
auto plus_ten ( [=] (int x) { return plus(10, x); } );
std::cout << plus_ten(5) << 'n';
}
- Before compiling and running the program, go through the code again and try to foresee what it will print to the terminal. Then run it and check against the real output:
1, 2
3
ab
3
1, 2, 3, 4, 5,
Value of a after 3 incrementer() calls: 3
15