EXPLORATION 23

image

Unnamed Functions

One problem with calling algorithms is that sometimes the predicate or transformation function must be declared far from the place where it is called. With a properly descriptive name, this problem can be reduced, but often the function is trivial, and your program would be much easier to read if you could put its functionality directly in the call to the standard algorithm. A new feature in C++ 11 permits exactly that.

Lambdas

C++ 11 lets you define a function as an expression. You can pass this function to an algorithm, save it in a variable, or call it immediately. Such a function is called a lambda, for reasons that only a computer scientist would understand or even care about. If you aren’t a computer scientist, don’t worry, and just realize that when the nerds talk about lambdas, they are just talking about unnamed functions. As a quick introduction, Listing 23-1 rewrites Listing 22-1 to use a lambda.

Listing 23-1.  Calling transform to Apply a Lambda to Each Element of an Array

#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
 
int main()
{
   std::vector<int> data{};
 
   std::copy(std::istream_iterator<int>(std::cin),
             std::istream_iterator<int>(),
             std::back_inserter(data));
 
   std::transform(data.begin(), data.end(), data.begin(), [](int x) { return x * 2; });
 
   std::copy(data.begin(), data.end(),
             std::ostream_iterator<int>(std::cout, " "));
}

The lambda almost looks like a function definition. Instead of a function name, a lambda begins with square brackets. The usual function arguments and compound statement follow. What’s missing? _______________________________________

That’s right, the function’s return type. When a lambda’s function body contains only a single return statement, the compiler deduces the function’s type from the type of the return expression. In this case, the return type is int.

With a lambda, the program is slightly shorter and much easier to read. You don’t have to hunt for the definition of times_two() to learn what it does. (Not all functions are named so clearly.) But lambdas are even more powerful and can do things that ordinary functions can’t. Take a look at Listing 23-2 to see what I mean.

Listing 23-2.  Using a Lambda to Access Local Variables

#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
 
int main()
{
   std::vector<int> data{};
 
   std::cout << "Multiplier: ";
   int multiplier{};
   std::cin >> multiplier;
   std::cout << "Data: ";
   std::copy(std::istream_iterator<int>(std::cin),
      std::istream_iterator<int>(),
      std::back_inserter(data));
 
   std::transform(data.begin(), data.end(), data.begin(),
      [multiplier](int i){ return i * multiplier; });
 
   std::copy(data.begin(), data.end(),
      std::ostream_iterator<int>(std::cout, " "));
}

Predict the output if the program’s input is as follows:

4 1 2 3 4 5

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

The first number is the multiplier, and the remaining numbers are multiplied by it, yielding

4
8
12
16
20

See the trick? The lambda is able to read the local variable, multiplier. A separate function, such as times_two() in Listing 22-1, can’t do that. Of course, you can pass two arguments to times_two(), but this use of the transform algorithm calls the function with only one argument. There are ways to work around this limitation, but I won’t bother showing them to you, because lambdas solve the problem simply and elegantly.

Naming an Unnamed Function

Although a lambda is an unnamed function, you can give it a name by assigning the lambda to a variable. This is a case where you probably want to declare the variable using the auto keyword, so you don’t have to think about the type of thelambda that is the variable’s initial value. Just remember that auto doesn’t play well with universal initialization, so use an equal sign or parentheses, as in the following example:

auto times_three = [](int i) { return i * 3; };

Once you assign a lambda to a variable, you can call that variable as though it were an ordinary function:

int forty_two{ times_three(14) };

The advantage of naming a lambda is that you can call it more than once in the same function. In this way, you get the benefit of self-documenting code with a well-chosen name and the benefit of a local definition.

If you don’t want to use auto, the standard library can help. In the <functional> header is the type std::function, which you use by placing a function’s return type and parameter types in angle brackets, e.g., std::function<int(int)>. For example, the following defines a variable, times_two, and initializes it with a lambda that takes one argument of type int and returns int:

std::function<int(int)> times_two{ [](int i) { return i * 2; } };

The actual type of a lambda is more complicated, but the compiler knows how to convert that type to the matching std::function<> type.

Capturing Local Variables

Naming a local variable in the lambda’s square brackets is called capturing the variable. If you do not capture a variable, you cannot use it in the lambda, so the lambda would be able to use only its function parameters.

Read the program in Listing 23-3. Think about how it captures the local variable, multiplier.

Listing 23-3.  Using a Lambda to Access Local Variables

#include <algorithm>
#include <iostream>
#include <iterator>
#include <vector>
 
int main()
{
   std::vector<int> data{ 1, 2, 3 };
 
   int multiplier{3};
   auto times = [multiplier](int i) { return i * multiplier; };
 
   std::transform(data.begin(), data.end(), data.begin(), times);
 
   multiplier = 20;
   std::transform(data.begin(), data.end(), data.begin(), times);
 
   std::copy(data.begin(), data.end(),
             std::ostream_iterator<int>(std::cout, " "));
}

Predict the output from Listing 23-3.

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

_____________________________________________________________

Now run the program. Was your prediction correct? ________________ Why or why not?

_____________________________________________________________

_____________________________________________________________

The value of multiplier was captured by value when the lambda was defined. Thus, changing the value of multiplier later does not change the lambda, and it still multiplies by three. The transform() algorithm is called twice, so the effect is to multiply by 9, not 60.

If you need your lambda to keep track of the local variable and always use its most recent value, you can capture a variable by reference by prefacing its name with an ampersand (similar to a reference function parameter), as in the following example:

[&multiplier](int i) { return i * multiplier; };

Modify Listing 23-3 to capture multiplier by reference. Run the program to observe its new behavior.

You can choose to omit the capture name to capture all local variables. Use an equal sign to capture everything by value or just an ampersand to capture everything by reference.

int x{0}, y{1}, z{2};
auto capture_all_by_value = [=]() { return x + y + z; };
auto capture_all_by_reference = [&]() { x = y = z = 0; };

I advise against capturing everything by default, because it leads to sloppy code. Be explicit about the variables that the lambda captures. The list of captures should be short, or else you are probably doing something wrong. Nonetheless, you will likely see other programmers capture everything, if only out of laziness, so I had to show you the syntax.

If you follow best practices and list individual capture names, the default is capture-by-value, so you must supply an ampersand for each name that you want to capture by reference. Feel free to mix capture-by-value and capture-by-reference.

auto lambda =
   [by_value, &by_reference, another_by_value, &another_by_reference]() {
      by_reference = by_value;
      another_by_reference = another_by_value;
   };

const Capture

Capture-by-value has one trick up its sleeve that can take you by surprise. Consider the simple program in Listing 23-4.

Listing 23-4.  Using a Lambda to Access Local Variables

#include <iostream>
 
int main()
{
   int x{0};
   auto lambda = [x](int y) {
      x = 1;
      y = 2;
      return x + y;
   };
   int local{0};
   std::cout << lambda(local) << ", " << x << ", " << local << ' ';
 
}

What do you expect to happen when you run this program?

_____________________________________________________________What is the surprise?

_____________________________________________________________

You already know that function parameters are call-by-value, so the y = 1 assignment has no effect outside of the lambda, and local remains 0. A by-value capture is similar in that you cannot change the local variable that is captured (x in Listing 23-4). But the compiler is even pickier than that. It doesn’t let you write the assignment x = 1. It’s as though every by-value capture were declared const.

Lambdas are different from ordinary functions in that default for by-value captures is const, and to get a non-const capture, you must explicitly tell the compiler. The keyword to use is mutable, which you put after the function parameters, as shown in Listing 23-5.

Listing 23-5.  Using the mutable Keyword in a Lambda

#include <iostream>
 
int main()
{
   int x{0};
   auto lambda = [x](int y) mutable{
      x = 1;
      y = 2;
      return x + y;
   };
   int local{0};
   std::cout << lambda(local) << ", " << x << ", " << local << ' ';
}

Now the compiler lets you assign to the capture, x. The capture is still by-value, so x in main() doesn’t change. The output of the program is

3, 0, 0

So far, I have never found an instance when I wanted to use mutable. It’s there if you need it, but you will probably never need it.

Return Type

The return type of a lambda is the type of the return expression, if the lambda body contains only a return statement. But what if the lambda is more complicated? Although some argue that the compiler should be able to deduce the type from a more complicated lambda (with the restriction that every return statement have the same type), that is not what the standard says. Future updates to the standard will loosen this restriction, but as I write this, the restriction remains in place.

But the syntax for a lambda does not lend itself to declaring a function return type in the usual way. Instead, the return type follows the function parameter list, with an arrow (->) between the closing parenthesis and the return type:

[](int i) -> int { return i * 2; }

In general, the lambda is easier to read without the explicit return type. The return type is usually obvious, but if it is not, go ahead and be explicit. Clarity trumps brevity.

Rewrite Listing 22-5 to take advantage of lambdas. Write functions where you think functions are appropriate, and write lambdas where you think lambdas are appropriate. Compare your solution with mine in Listing 23-6.

Listing 23-6.  Testing for Palindromes

#include <algorithm>
#include <iostream>
#include <iterator>
#include <locale>
#include <string>
 
/** Determine whether @p str is a palindrome.
 * Only letter characters are tested. Spaces and punctuation don't count.
 * Empty strings are not palindromes because that's just too easy.
 * @param str the string to test
 * @return true if @p str is the same forward and backward
 */
bool is_palindrome(std::string str)
{
  // Filter the string to keep only letters
  std::string::iterator end{std::remove_if(str.begin(), str.end(),
    [](char ch)
    {
      return not std::isalpha(ch, std::locale());
    })
  };
 
  // Reverse the filtered string.
  std::string rev{str.begin(), end};
  std::reverse(rev.begin(), rev.end());
 
  // Compare the filtered string with its reversal, without regard to case.
  return not rev.empty() and std::equal(str.begin(), end, rev.begin(),
    [](char a, char b)
    {
        auto lowercase = [](char ch)
        {
           return std::tolower(ch, std::locale());
        };
        return lowercase(a) == lowercase(b);
    }
  );
}
 
int main()
{
  std::locale::global(std::locale{""});
  std::cin.imbue(std::locale{});
  std::cout.imbue(std::locale{});
 
  std::string line{};
  while (std::getline(std::cin, line))
    if (is_palindrome(line))
      std::cout << line << ' ';
}

You may have noticed that some algorithms have more than one form. The sort function, for example, can take two iterators as arguments, or it can take two iterators and a predicate. Using one name for more than one function is called overloading. This is the subject of the next Exploration.

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

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