10.3.2. Lambda Expressions

The predicates we pass to an algorithm must have exactly one or two parameters, depending on whether the algorithm takes a unary or binary predicate, respectively. However, sometimes we want to do processing that requires more arguments than the algorithm’s predicate allows. For example, the solution you wrote for the last exercise in the previous section had to hard-wire the size 5 into the predicate used to partition the sequence. It would be move useful to be able to partition a sequence without having to write a separate predicate for every possible size.

As a related example, we’ll revise our program from § 10.3.1 (p. 387) to report how many words are of a given size or greater. We’ll also change the output so that it prints only the words of the given length or greater.

A sketch of this function, which we’ll name biggies, is as follows:

void biggies(vector<string> &words,
             vector<string>::size_type sz)
{
    elimDups(words); // put words in alphabetical order and remove duplicates
    // resort by length, maintaining alphabetical order among words of the same length
    stable_sort(words.begin(), words.end(), isShorter);
    // get an iterator to the first element whose size() is >= sz
    // compute the number of elements with size >= sz
    // print words of the given size or longer, each one followed by a space
}

Our new problem is to find the first element in the vector that has the given size. Once we know that element, we can use its position to compute how many elements have that size or greater.

We can use the library find_if algorithm to find an element that has a particular size. Like find10.1, p. 376), the find_if algorithm takes a pair of iterators denoting a range. Unlike find, the third argument to find_if is a predicate. The find_if algorithm calls the given predicate on each element in the input range. It returns the first element for which the predicate returns a nonzero value, or its end iterator if no such element is found.

It would be easy to write a function that takes a string and a size and returns a bool indicating whether the size of a given string is greater than the given size. However, find_if takes a unary predicate—any function we pass to find_if must have exactly one parameter that can be called with an element from the input sequence. There is no way to pass a second argument representing the size. To solve this part of our problem we’ll need to use some additional language facilities.

Introducing Lambdas

We can pass any kind of callable object to an algorithm. An object or expression is callable if we can apply the call operator (§ 1.5.2, p. 23) to it. That is, if e is a callable expression, we can write e(args) where args is a comma-separated list of zero or more arguments.

The only callables we’ve used so far are functions and function pointers (§ 6.7, p. 247). There are two other kinds of callables: classes that overload the function-call operator, which we’ll cover in § 14.8 (p. 571), and lambda expressions.

Image

A lambda expression represents a callable unit of code. It can be thought of as an unnamed, inline function. Like any function, a lambda has a return type, a parameter list, and a function body. Unlike a function, lambdas may be defined inside a function. A lamba expression has the form

[capture list] (parameter list) -> return type  { function body }

where capture list is an (often empty) list of local variables defined in the enclosing function; return type, parameter list, and function body are the same as in any ordinary function. However, unlike ordinary functions, a lambda must use a trailing return (§ 6.3.3, p. 229) to specify its return type.

We can omit either or both of the parameter list and return type but must always include the capture list and function body:

auto f = [] { return 42; };

Here, we’ve defined f as a callable object that takes no arguments and returns 42.

We call a lambda the same way we call a function by using the call operator:

cout << f() << endl;  // prints 42

Omitting the parentheses and the parameter list in a lambda is equivalent to specifying an empty parameter list. Hence, when we call f, the argument list is empty. If we omit the return type, the lambda has an inferred return type that depends on the code in the function body. If the function body is just a return statement, the return type is inferred from the type of the expression that is returned. Otherwise, the return type is void.


Image Note

Lambdas with function bodies that contain anything other than a single return statement that do not specify a return type return void.


Passing Arguments to a Lambda

As with an ordinary function call, the arguments in a call to a lambda are used to initialize the lambda’s parameters. As usual, the argument and parameter types must match. Unlike ordinary functions, a lambda may not have default arguments (§ 6.5.1, p. 236). Therefore, a call to a lambda always has as many arguments as the lambda has parameters. Once the parameters are initialized, the function body executes.

As an example of a lambda that takes arguments, we can write a lambda that behaves like our isShorter function:

[](const string &a, const string &b)
  { return a.size() < b.size();}

The empty capture list indicates that this lambda will not use any local variables from the surrounding function. The lambda’s parameters, like the parameters to isShorter, are references to const string. Again like isShorter, the lambda’s function body compares its parameters’ size()s and returns a bool that depends on the relative sizes of the given arguments.

We can rewrite our call to stable_sort to use this lambda as follows:

// sort words by size, but maintain alphabetical order for words of the same size
stable_sort(words.begin(), words.end(),
            [](const string &a, const string &b)
              { return a.size() < b.size();});

When stable_sort needs to compare two elements, it will call the given lambda expression.

Using the Capture List

We’re now ready to solve our original problem, which is to write a callable expression that we can pass to find_if. We want an expression that will compare the length of each string in the input sequence with the value of the sz parameter in the biggies function.

Although a lambda may appear inside a function, it can use variables local to that function only if it specifies which variables it intends to use. A lambda specifies the variables it will use by including those local variables in its capture list. The capture list directs the lambda to include information needed to access those variables within the lambda itself.

In this case, our lambda will capture sz and will have a single string parameter. The body of our lambda will compare the given string’s size with the captured value of sz:

[sz](const string &a)
    { return a.size() >= sz; };

Inside the [] that begins a lambda we can provide a comma-separated list of names defined in the surrounding function.

Because this lambda captures sz, the body of the lambda may use sz. The lambda does not capture words, and so has no access to that variable. Had we given our lambda an empty capture list, our code would not compile:

// error: sz not captured
[](const string &a)
    { return a.size() >= sz; };


Image Note

A lambda may use a variable local to its surrounding function only if the lambda captures that variable in its capture list.


Calling find_if

Using this lambda, we can find the first element whose size is at least as big as sz:

// get an iterator to the first element whose size() is >= sz
auto wc = find_if(words.begin(), words.end(),
            [sz](const string &a)
                { return a.size() >= sz; });

The call to find_if returns an iterator to the first element that is at least as long as the given sz, or a copy of words.end() if no such element exists.

We can use the iterator returned from find_if to compute how many elements appear between that iterator and the end of words3.4.2, p. 111):

// compute the number of elements with size >= sz
auto count = words.end() - wc;
cout << count << " " << make_plural(count, "word", "s")
     << " of length " << sz << " or longer" << endl;

Our output statement calls make_plural6.3.2, p. 224) to print word or words, depending on whether that size is equal to 1.

The for_each Algorithm

The last part of our problem is to print the elements in words that have length sz or greater. To do so, we’ll use the for_each algorithm. This algorithm takes a callable object and calls that object on each element in the input range:

// print words of the given size or longer, each one followed by a space
for_each(wc, words.end(),
         [](const string &s){cout << s << " ";});
cout << endl;

The capture list in this lambda is empty, yet the body uses two names: its own parameter, named s, and cout.

The capture list is empty, because we use the capture list only for (nonstatic) variables defined in the surrounding function. A lambda can use names that are defined outside the function in which the lambda appears. In this case, cout is not a name defined locally in biggies; that name is defined in the iostream header. So long as the iostream header is included in the scope in which biggies appears, our lambda can use cout.


Image Note

The capture list is used for local nonstatic variables only; lambdas can use local statics and variables declared outside the function directly.


Putting It All Together

Now that we’ve looked at the program in detail, here is the program as a whole:

void biggies(vector<string> &words,
             vector<string>::size_type sz)
{
    elimDups(words); // put words in alphabetical order and remove duplicates
    // sort words by size, but maintain alphabetical order for words of the same size
    stable_sort(words.begin(), words.end(),
                [](const string &a, const string &b)
                  { return a.size() < b.size();});
    // get an iterator to the first element whose size() is >= sz
    auto wc = find_if(words.begin(), words.end(),
                [sz](const string &a)
                    { return a.size() >= sz; });
    // compute the number of elements with size >= sz
    auto count = words.end() - wc;
    cout << count << " " << make_plural(count, "word", "s")
         << " of length " << sz << " or longer" << endl;
    // print words of the given size or longer, each one followed by a space
    for_each(wc, words.end(),
             [](const string &s){cout << s << " ";});
    cout << endl;
}


Exercises Section 10.3.2

Exercise 10.14: Write a lambda that takes two ints and returns their sum.

Exercise 10.15: Write a lambda that captures an int from its enclosing function and takes an int parameter. The lambda should return the sum of the captured int and the int parameter.

Exercise 10.16: Write your own version of the biggies function using lambdas.

Exercise 10.17: Rewrite exercise 10.12 from § 10.3.1 (p. 387) to use a lambda in the call to sort instead of the compareIsbn function.

Exercise 10.18: Rewrite biggies to use partition instead of find_if. We described the partition algorithm in exercise 10.13 in § 10.3.1 (p. 387).

Exercise 10.19: Rewrite the previous exercise to use stable_partition, which like stable_sort maintains the original element order in the paritioned sequence.


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

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