Lesson 22. Lambda Expressions

Lambda expressions, which were introduced in C++11 and have been improved in more recent versions of C++, define function objects without requiring a class or a struct with a name. Recent amendments in C++20 add support for template arguments.

In this lesson, you find out

• How to program a lambda expression

• How to use lambda expressions as predicates

• What generic lambda expressions are

• How to program lambda expressions that can hold and manipulate state

What Is a Lambda Expression?

A lambda expression can be visualized as a compact version of an unnamed struct (or class) with a public operator() function. In that sense, a lambda expression is a function object. Before we get into programming lambda functions, let’s revisit a function object from Listing 21.1 (from Lesson 21, “Understanding Function Objects") as an example:

// struct that behaves as a unary function
template <typename elementType>
struct DisplayElement
{
    void operator()(const elementType& element) const
    {
        cout << element << ’ ’;
    }
};

This function object displays the object element on the screen by using cout. It would be used in algorithms such as std::for_each(), as shown in this example:

// Display every integer contained in a vector
for_each(numsInVec.cbegin(),    // Start of range
         numsInVec.cend(),      // End of range
         DisplayElement<int>()); // Unary function object

A lambda equivalent of struct DisplayElement requires a single line of code:

auto lambda = [](const int& element) {cout << element << ’ ’; };

This is what it looks like when used with for_each():

// Display elements in vector using a lambda expression
for_each(numsInVec.cbegin(),    // Start of range
         numsInVec.cend(),      // End of range
         lambda);  // display elements

Tip

Lambda expressions are also called lambda functions.


How to Define a Lambda Expression

A lambda expression always starts with square brackets ([]). C++20 introduced new capabilities such as support for template arguments to lambda expressions, making them more generic and, hence, more powerful.

Lambda expression syntax can be generalized to:

[optional captured variables]<optional template arguments> optional-lambda-specifiers (arguments)
{ // lambda expression code; }

Capturing Variables

If you want to use variables that are declared outside the body of a lambda expression, you need to capture these variables. The capture list can include multiple variables separated by commas:

[var1, var2] <class Type> (Type& param) { // lambda code here; }

If you need to modify these variables within the lambda expression, you add the specifier mutable:

[var1, var2] <class Type> (Type& param) mutable
{ // lambda code here; }

Caution

A mutable lambda expression allows captured variables ([...]) to be modified within the lambda expression, but these changes are not reflected outside the expression.


If modifications to the captured variables within the lambda expression are required to be reflected outside it, too, then you use references:

[&var1, &var2] <class Type> (Type& param) { // lambda code here; }

Tip

In addition to mutable, the other two specifiers that optionally enhance a lambda expression are constexpr and consteval. constexpr indicates the intention to have the lambda evaluated as a constant expression where possible, whereas consteval makes the lambda an immediate function to be evaluated by the compiler.


Parameters

C++20 allows lambda expressions to be extremely generic—akin to function objects programmed using template classes. You may optionally specify a template parameter list, as shown here:

[var1, var2] <typename Type1, typename Type2> (Type1 param1, Type2 param2)
{ // lambda code here; }

In addition, lambda expressions permit multiple input parameters, separated by commas:

[var1, var2] <class Type> (Type param1, Type param2)
{ // lambda code here; }

With automatic type deduction using the keyword auto, you can program a generic lambda expression that looks like this:

[var1, var2] (auto param1, auto param2)
{ // lambda code here; }

Return Types

A lambda expression that comprises a single return statement does not need to explicitly specify a return value type because the type is automatically deduced by the compiler. To explicitly declare a return type, you use -> as follows:

[var1, var2] <class Type> (Type param1, Type param2) -> ReturnType
{ return (value or expression ); }

Finally, the compound statement {} can hold multiple statements, each separated by a ;, as shown here:

[stateVar1, stateVar2](Type1 var1, Type2 var2) -> ReturnType
{
   Statement 1;
   Statement 2;
   return (value or expression);
}

A Lambda Expression for a Unary Function

A simple lambda version of the unary operator(Type) function that takes one parameter would be the following:

[](Type paramName) {  // lambda expression code;  }

Note that you can pass the parameter by reference if you so wish:

[](Type& paramName) {  // lambda expression code here;  }

Use Listing 22.1 to study the usage of a lambda function in displaying the contents of a Standard Template Library (STL) container using the algorithm for_each().

Input

Listing 22.1 Displaying Elements in a Container via the Algorithm for_each()Invoked with a Lambda Expression Instead of a Function Object

 0: #include <algorithm>
 1: #include <iostream>
 2: #include <vector>
 3: #include <list>
 4: using namespace std;
 5:
 6: int main()
 7: {
 8:    vector<int> numsInVec{ 101, -4, 500, 21, 42, -1 };
 9:    cout << “Display elements in a vector using a lambda: “ << endl;
10:
11:    // Display the array of integers
12:    for_each(numsInVec.cbegin(),    // Start of range
13:             numsInVec.cend(),        // End of range
14:             [](const int& element) {cout << element << ’ ’; }); // lambda
15:
16:    cout << endl;
17:
18:    list<char> charsInList{ ’a’, ’h’, ’z’, ’k’, ’l’ };
19:    cout << “Display elements in a list using a lambda: “ << endl;
20:
21:    // Display the list of characters
22:    for_each(charsInList.cbegin(),     // Start of range
23:             charsInList.cend(),        // End of range
24:             [](auto& element) {cout << element << ’ ’; }); // lambda
25:
26:    return 0;
27: }

Output

Display elements in a vector using a lambda:
101 -4 500 21 42 -1
Display elements in a list using a lambda:
a h z k l

Analysis

There are two lambda expressions of interest, in Lines 14 and 24. They do similar tasks and are the same except for the type of the input parameter. They have been customized to the nature of the elements within the two containers. The first expression takes one parameter that is an int, as it is used to print one element at a time from a vector of integers’ the second expression accepts a char (automatically deduced by the compiler) as it is used to display elements of type char stored in a std::list.


Tip

You might have noticed that the second lambda expression in Listing 22.1 is slightly different:

for_each(charsInList.cbegin(),    // Start of range
         charsInList.cend(),      // End of range
 [](auto& element) {cout << element << ’ ’; } ); // lambda

This lambda expression uses the compiler’s automatic type deduction capabilities, which are invoked using the keyword auto. The compiler would interpret this lambda expression as

for_each(charsInList.cbegin(),    // Start of range
         charsInList.cend(),      // End of range
 [](const char& element) {cout << element << ’ ’; } );


Note

The code in Listing 22.1 is similar to that in Listing 21.1 except that the latter uses function objects. In fact, Listing 22.1 is a lambda version of the function object DisplayElement<T>.

If you compare the two listings, you can see that lambda functions make C++ code simple and compact.


A Lambda Expression for a Unary Predicate

A predicate helps make decisions. A unary predicate is a unary expression that returns a bool, conveying true or false. Lambda expressions can return values, too. For example, the following code is a lambda expression that returns true for numbers that are even:

[](int& num) {return ((num % 2) == 0); }

The nature of the return value in this case tells the compiler that the lambda expression returns a bool.

You can use a lambda expression that is a unary predicate in algorithms, such as std::find_if(), to find even numbers in a collection. See Listing 22.2 for an example.

Input

Listing 22.2 Finding an Even Number in a Collection Using a Lambda Expression for a Unary Predicate and the Algorithm std::find_if()

 0: #include<algorithm>
 1: #include<vector>
 2: #include<iostream>
 3: using namespace std;
 4:
 5: int main()
 6: {
 7:    vector<int> numsInVec{ 25, 101, 2017, -50 };
 8:
 9:    auto evenNum = find_if(numsInVec.cbegin(),
10:                         numsInVec.cend(),  // range to find in
11:                  [](const int& num){return ((num % 2) == 0); } );
12:
13:    if (evenNum != numsInVec.cend())
14:       cout << “Even number in collection is: “ << *evenNum << endl;
15:
16:    return 0;
17: }

Output

Even number in collection is: -50

Analysis

The lambda function that works as a unary predicate is shown in Line 11. The algorithm find_if() invokes the unary predicate for every element in the range. When the predicate returns true, find_if() reports a find by returning an iterator evenNum to that element. The predicate in this case is the lambda expression that returns true when find_if() invokes it with an integer that is even; that is, the result of a modulo operation with 2 is zero.


Tip

Listings 22.1 and 22.2 demonstrate not only a lambda expression as a unary predicate but also the use of const within a lambda expression.

Remember to use const for input parameters, especially when they’re references, to avoid unintentional changes to the value of elements in a container.


A Lambda Expression with State via Capture Lists ([...])

Listing 22.2 shows a unary predicate that returns true if an integer is divisible by 2—that is, if the integer is an even number. What if you need a more generic function that returns true when the number is divisible by a divisor of the user’s choosing? You need to maintain that “state”—that is, the divisor—in the expression:

int divisor = 2; // initial value
...
auto element = find_if(begin of a range,
                      end of a range,
       [divisor](int dividend){return (dividend % divisor) == 0; } );

A list of arguments transferred as state variables ([...]) is also called the lambda’s capture list.


Note

The lambda expression shown above is a one-line equivalent of the 16 lines of code in Listing 21.3 that define the unary predicate struct IsMultiple<>.


Listing 22.3 demonstrates the application of a unary predicate, given a state variable, in finding a number in a collection that is a multiple of a divisor supplied by the user.

Input

Listing 22.3 Demonstrating the Use of Lambda Expressions That Hold State to Check Whether One Number Is Divisible by Another

 0: #include <algorithm>
 1: #include <vector>
 2: #include <iostream>
 3: using namespace std;
 4:
 5: int main()
 6: {
 7:    vector<int> numsInVec{25, 26, 27, 28, 29, 30, 31};
 8:    cout << “The vector contains: {25, 26, 27, 28, 29, 30, 31}”;
 9:
10:    cout << endl << “Enter divisor (> 0): “;
11:    int divisor = 2;
12:    cin >> divisor;
13:
14:    // Find the first element that is a multiple of divisor
15:    vector<int>::iterator element;
16:    element = find_if(numsInVec.begin(),
17:                      numsInVec.end(),
18:           [divisor](int dividend){return (dividend % divisor) == 0; } );
19:
20:    if(element != numsInVec.end())
21:    {
22:       cout << “First element in vector divisible by “ << divisor;
23:       cout << “: “ << *element << endl;
24:    }
25:
26:    return 0;
27: }

Output

The vector contains: {25, 26, 27, 28, 29, 30, 31}
Enter divisor (> 0): 4
First element in vector divisible by 4: 28

Analysis

The lambda expression that contains state and works as a predicate is shown in Line 18. divisor is the state variable, and it is comparable to IsMultiple::Divisor in Listing 21.3. Hence, state variables are akin to member attributes in a function object class. You are now able to pass states on to your lambda function and customize its usage on the basis of the state.


Note

Listing 22.3 features the lambda expression equivalent of Listing 21.4, without the function object class IsMultiple. This lambda expression eliminated 16 lines of code!


A Lambda Expression for a Binary Function

A binary function takes two parameters and optionally returns a value. A lambda expression equivalent of a binary function would be

[...](Type1& param1Name, Type2& param2Name)
{  // lambda code here;  }

Listing 22.4 shows a lambda expression that multiplies two equal-sized vectors element by element, using std::transform(), and stores the result in a third vector.

Input

Listing 22.4 Using a Lambda Expression as a Binary Function to Multiply Elements from Two Containers and Store the Result in a Third

 0: #include <vector>
 1: #include <iostream>
 2: #include <algorithm>
 3:
 4: int main()
 5: {
 6:    using namespace std;
 7:
 8:    vector<int> vecMultiplicand{ 0, 1, 2, 3, 4 };
 9:    vector<int> vecMultiplier{ 100, 101, 102, 103, 104 };
10:
11:    // Holds the result of multiplication
12:    vector<int> vecResult;
13:
14:    // Make space for the result of the multiplication
15:    vecResult.resize(vecMultiplier.size());
16:
17:    transform(vecMultiplicand.begin(), // range of multiplicands
18:              vecMultiplicand.end(), // end of range
19:              vecMultiplier.begin(),  // multiplier values
20:               vecResult.begin(), // range that holds result
21:               [](int a, int b) {return a * b; } );  // lambda
22:
23:    cout << “The contents of the first vector are: “ << endl;
24:    for(size_t index = 0; index < vecMultiplicand.size(); ++index)
25:    cout << vecMultiplicand[index] << ’ ’;
26:    cout << endl;
27:
28:    cout << “The contents of the second vector are: “ << endl;
29:    for(size_t index = 0; index < vecMultiplier.size(); ++index)
30:    cout << vecMultiplier[index] << ’ ’;
31:    cout << endl;
32:
33:    cout << “The result of the multiplication is: “ << endl;
34:    for(size_t index = 0; index < vecResult.size(); ++index)
35:    cout << vecResult[index] << ’ ’;
36:
37:    return 0;
38:  }

Output

The contents of the first vector are:
0 1 2 3 4
The contents of the second vector are:
100 101 102 103 104
The result of the multiplication is:
0 101 204 309 416

Analysis

The lambda expression in this example is shown in Line 21 as a parameter to std::transform(). This algorithm takes two ranges as input and applies a transformation algorithm that is contained in a binary function (the lambda expression of interest). The return value of the binary function is stored in a target container. The lambda expression accepts two integers as input and returns the result of the multiplication via the return value. This return value is stored by std::transform() in vecResult. The output demonstrates the contents of the two containers and the result of multiplying them element by element.


Note

Listing 22.4 demonstrates the lambda equivalent of the function object class Multiply<> from Listing 21.5.


A Lambda Expression for a Binary Predicate

A function that accepts two parameters and returns either true or false to help make a decision is called a binary predicate. Binary predicates find use in sort algorithms, such as std::sort(), that invoke the binary predicate for any two values in a container to determine which one should be placed after the other. The generic syntax of a binary predicate is

[...](Type1& param1Name, Type2& param2Name) {  // return bool expression; }

Listing 22.5 demonstrates a lambda expression being used with template parameters and another being used as a binary predicate with an explicit return type declaration.

Input

Listing 22.5 Using a Lambda Expression as a Binary Predicate in std::sort() to Enable Case-Insensitive Sorting

 0: #include <algorithm>
 1: #include <string>
 2: #include <vector>
 3: #include <iostream>
 4: using namespace std;
 5:
 6: int main()
 7: {
 8:    vector <string> namesInVe”{ “”im”, “J”ck”, “”am”, “A”na” };
 9:
10:    // template lambda that displays object on screen
11:    auto displayElement = []<typename T>(const T& element)
12:                           { cout << element ’<’’ ’;};
13:
14:    cout << “The names in vector in order of insertion: “ << endl;
15:    for_each(namesInVec.cbegin(), namesInVec.cend(), displayElement);
16:
17:    cout “< “
Order after case sensitive sort:”
”;
18:    sort(namesInVec.begin(), namesInVec.end());
19:    for_each(namesInVec.cbegin(), namesInVec.cend(), displayElement);
20:
21:    cout “< “
Order after sort ignoring case:”
”;
22:    sort(namesInVec.begin(), namesInVec.end(),
23:        [](const string& str1, const string& str2) -> bool  // lambda
24:        {
25:           string str1LC, str2LC; // LC = lowercase
26:           str1LC.resize(str1.size()); // create space to store result
27:           str2LC.resize(str2.size());
28:
29:           // Convert strings (each character) to the lower case
30:           transform(str1.begin(), str1.end(), str1LC.begin(),::tolower);
31:           transform(str2.begin(), str2.end(), str2LC.begin(), ::tolower);
32:
33:           return(str1LC < str2LC);
34:        }  // end of lambda
35:      );  // end of sort
36:
37:    for_each(namesInVec.cbegin(), namesInVec.cend(), displayElement);
38:
39:    return 0;
40: }

Output

The names in vector in order of insertion:
jim Jack Sam Anna
Order after case sensitive sort:
Anna Jack Sam jim
Order after sort ignoring case:
Anna Jack jim Sam

Analysis

Listing 22.5 is quite novel in that it uses a template lambda expression displayElement, which is defined in Lines 11 and 12 and used by the algorithm for_each() in Lines 15, 19 and 37. displayElement is the lambda equivalent of struct DisplayContents<>, which you have used multiple times (for instance, in Listing 18.2 to display elements in std::list and in Listing 19.2 to display elements in std::set). This one-line lambda expression is generic and powerful.

A rather large lambda expression also spans Lines 23 through 34 as the third parameter of std::sort()! A lambda that spans multiple statements needs to explicitly declare the return value type (bool), as shown in Line 23. This lambda expression implements a binary predicate that assists with case-insensitive sorting by first converting the two strings to be compared to the same case (in this example, to lowercase, as shown in Lines 30 and 31) and then comparing the converted strings with each other. This case-insensitive sort helps place "jim" after "Jack", as shown in the output.


Note

This extraordinarily large lambda expression in Listing 22.5 is a lambda version of class CompareStringNoCase from Listing 21.6, which is used in Listing 21.7.


Images

Summary

In this lesson, you learned about an important feature that contributes to modern C++ programming: lambda expressions. You saw that lambda expressions are basically unnamed function objects that can take parameters, that have state, that return values, and that can be multiple lines. You learned how to use lambdas instead of function objects in STL algorithms, such as find(), sort(), or transform(). Lambda expressions make programming in C++ fast and efficient, and you should try to use them where applicable.

Q&A

Q. How are the state parameters of a lambda expression transferred: by value or by reference?

A. When a lambda expressions is programmed with a capture list, like this:

[var1, var2, ... varN](Type& param1, ... ) { ...expression ;}

the state parameters var1 and var2 are copied (that is, not supplied as a reference). If you want to have them as reference parameters, you use this syntax:

[&Var1, &Var2, ... &varN](Type& param1, ... ) { ...expression ;}

In this case, you need to exercise caution as modifications to the state variables supplied within the capture list continue outside the lambda expression.

Q. Can I use the local variables in a function in a lambda expression?

A. You can pass the local variables in the capture list:

[var1, var2, ... varN](Type& param1, ... ) { ...expression ;}

If you need to capture all variables, you use this syntax:

[=](Type& Param1, ... ) { ...expression ;}

Workshop

The Workshop provides quiz questions to help you solidify your understanding of the material covered and exercises to provide you with experience using what you’ve learned. Try to answer the quiz and exercise questions before checking the answers in Appendix E and be certain you understand the answers before continuing to the next lesson.

Quiz

1. How does a compiler recognize the start of a lambda expression?

2. How do you pass state variables to a lambda function?

3. If you need to supply a return value in a lambda expression, how do you do it?

Exercises

1. Write a lambda binary predicate that helps sort elements in a container in descending order.

2. Write a lambda function that, when used in for_each(), adds a user-specified value in a container such as a vector.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.15.15