📜 ⬆️ ⬇️

The book "C ++ 17 STL. Standard Template Library »

image The book describes the work with containers, algorithms, auxiliary classes, lambda expressions and other interesting tools that are rich in modern C ++. Having mastered the material, you will be able to radically reconsider the usual approach to programming. The advantage of the publication is in the detailed description of the standard C ++ template library, STL. Its latest version was released in 2017. In the book you will find more than 90 of the most realistic examples that demonstrate the power of STL. Many of them will become basic building blocks for solving more universal tasks. Armed with this book, you can effectively use C ++ 17 to create high-quality and high-performance software that is applicable in various industries.

The following is an excerpt of “Lambda expressions”.

One of the important new features in C ++ 11 was lambda expressions. In C ++ 14 and C ++ 17, they received new features, and this made them even more powerful. But what is a lambda expression?

Lambda expressions or lambda functions create closures. A closure is a very general term for nameless objects that can be called as functions. To provide such an opportunity in C ++, such an object must implement a function call operator (), with or without parameters. Creating a similar object without lambda expressions before the appearance of C ++ 11 would look like this:
')
#include <iostream> #include <string> int main() { struct name_greeter { std::string name; void operator()() { std::cout << "Hello, " << name << '\n'; } }; name_greeter greet_john_doe {"John Doe"}; greet_john_doe(); } 

Instances of the name_greeter structure obviously contain a string. Please note: the type of this structure and the object are not nameless, unlike lambda expressions. In terms of closures, it can be argued that they capture the string. When the example instance is called as a function with no parameters, the string “Hello, John Doe” is displayed, since we specified a string with that name.

Starting from C ++ 11, it has become easier to create such closures:

 #include <iostream> int main() { auto greet_john_doe ([] { std::cout << "Hello, John Doe\n"; }); greet_john_doe(); } 

That's all. The whole name_greeter structure is replaced with a small [] {/ * to do something * /}, which at first glance looks strange, but in the next section we will consider all possible cases of its use.

Lambda expressions help keep the code generic and clean. They can be used as parameters for generalized algorithms to refine them when processing specific user-defined types. They can also be used to wrap work packages and data so that they can be run in streams or simply save work and postpone the execution of the packages themselves. After the advent of C ++ 11, many libraries were created that work with lambda expressions, since they became a natural part of the C ++ language. Another use of lambda expressions is metaprogramming, since they can be evaluated during program execution. However, we will not consider this question, since it does not relate to the topic of this book.

In the current chapter, we will rely to a large extent on individual functional programming patterns, which may seem strange to newbies and even experienced programmers who have not yet worked with such patterns. If in the following examples you see lambda expressions returning lambda expressions, which again return lambda expressions, then please do not get lost. We are somewhat beyond the scope of the usual programming style to prepare for working with the modern C ++ language, in which functional programming templates are encountered more and more often. If the code of an example looks too complicated, take the time to take a closer look at it. As soon as you figure it out, complex lambda expressions in real projects will no longer confuse you.

Dynamic definition of functions using lambda expressions


Using lambda expressions, you can encapsulate the code to call it later or even in another place, since it is allowed to copy. In addition, you can encapsulate the code several times with slightly different parameters, without having to implement a new function class for this task.

The lambda expression syntax looked new in C ++ 11, and by C ++ 17 it has changed somewhat. In this section, we will see what lambda expressions look like and what they mean.

How it's done


In this example, we will write a small program in which we will work with lambda expressions in order to understand the basic principles of interaction with them.

1. To work with lambda expressions, no library support is needed, but we will display messages on the console and use strings, so we need the appropriate header files:

 #include <iostream> #include <string> 

2. In this example, all the action takes place in the function main. We define two objects of functions that do not accept parameters, and return integer constants with values ​​1 and 2. Note: the return expression is surrounded by curly brackets {}, as is done in ordinary functions, and parentheses (), indicating a function without parameters , are optional, we do not specify them in the second lambda expression. But square brackets [] must be present:

 int main() { auto just_one ( [](){ return 1; } ); auto just_two ( [] { return 2; } ); 

3. Now you can call both objects of functions simply by writing the name of the variables that are stored in them, and adding parentheses. In this line, they cannot be distinguished from ordinary functions:

 std::cout << just_one() << ", " << just_two() << '\n'; 

Let's forget about them and define another function object, called plus, - it takes two parameters and returns their sum:

 auto plus ( [](auto l, auto r) { return l + r; } ); 

5. It is quite simple to use such an object, in this respect it is similar to any other binary function. We indicated that its parameters are of type auto, so that the object will work with all data types for which the + operator is defined, for example, with strings.

 std::cout << plus(1, 2) << '\n'; std::cout << plus(std::string{"a"}, "b") << '\n'; 

6. No need to save the lambda expression in a variable to use it. We can also define it in the place where it is needed, and then place the parameters for this expression in parentheses immediately after it (1, 2):

 std::cout << [](auto l, auto r){ return l + r; }(1, 2) << '\n'; 

7. Next, we define a closure that contains an integer counter. With each call, the value of this counter will increase by 1 and return the new value. To indicate that the closure contains an internal counter, put the expression count = 0 in parentheses - it indicates that the count variable is initialized with the integer value 0. To allow it to change its own variables, we use the mutable keyword, because otherwise the compiler will not allow do it:

 auto counter ( [count = 0] () mutable { return ++count; } ); 

8. Now call the function object five times and display the values ​​returned by it in order to see that the counter value increases:

 for (size_t i {0}; i < 5; ++i) { std::cout << counter() << ", "; } std::cout << '\n'; 

9. We can also take existing variables and grab them by reference instead of creating a copy of the value for the closure. Thus, the value of the variable will increase in the closure and at the same time will be available outside of it. To do this, we put the & a construct in brackets, where the & symbol means that we save the reference to the variable, but not the copy:

 int a {0}; auto incrementer ( [&a] { ++a; } ); 

10. If it works, then you can call this function object several times, and then see if the value of the variable a really changes:

 incrementer(); incrementer(); incrementer(); std::cout << "Value of 'a' after 3 incrementer() calls: " << a << '\n'; 

11. The last example demonstrates currying. It means that we take a function that takes some parameters, and then store it in another function object that takes fewer parameters. In this case, we save the plus function and accept only one parameter, which will be passed to the plus function. Another parameter is 10; we keep it in the function object. Thus, we get the function and call it plus_ten, because it can add the value 10 to the only parameter it takes.

 auto plus_ten ( [=] (int x) { return plus(10, x); } ); std::cout << plus_ten(5) << '\n'; } 

12. Before compiling and running the program, we will go through the code again and try to predict exactly which values ​​will be displayed in the terminal. Then run the program and take a look at the real output:

 1, 2 3 ab 3 1, 2, 3, 4, 5, Value of a after 3 incrementer() calls: 3 15 

How it works


What we have just done does not look too complicated: put the numbers together, then increment them and display them on the screen. We even performed string concatenation using the function object, which was implemented to add numbers. But for those who are still unfamiliar with the lambda expression syntax, this may seem confusing.

So, first consider all the features associated with lambda expressions (Fig. 4.1).

image

As a rule, you can omit most of these parameters to save some time. The shortest lambda expression is [] {}. It takes no parameters, captures nothing and, in fact, does nothing. What does the rest mean?

Capture List


Determines exactly what we are capturing and whether we are capturing at all. There are several ways to do this. Consider two "lazy" options.

1. If we write [=] () {...}, then we capture each external variable referenced by the closure by value; that is, these values ​​will be copied.

2. The record [&] () {...} means the following: all external objects referenced by the closure are captured only by reference, which does not lead to copying.

Of course, you can set the capture settings for each variable separately. The record [a, & b] () {...} means that we capture variable a by value, and variable b by reference. This will require more text to be printed, but as a rule, this method is safer, since we cannot accidentally capture something unnecessary from outside the circuit.

In the current example, we defined a lambda expression as follows: [count = 0] () {...}. In this special case, we are not capturing any variables from outside the closure limits, only defined a new variable named count. The type of this variable is determined based on the value with which we initialized it, namely, 0, so that it is of type int.

In addition, you can capture some variables by value, and others by reference, for example:

• [a, & b] () {...} - copy a and take a link to b;
• [&, a] () {...} - copy a and use the reference to any other transferred variable;
• [=, & b, i {22}, this] () {...} - we get a link to b, copy the value of this, initialize the new variable i with value 22 and copy any other used variable.

mutable (optional)
If the function object should be able to modify the variables it receives by copying ([=]), then it should be defined as mutable. The same applies to invoking non-constant methods of captured objects.

constexpr (optional)
If we explicitly mark a lambda with the constexpr keyword, the compiler will generate an error when this expression does not meet the criteria of the constexpr function. The advantage of using constexpr functions and lambda expressions is that the compiler can evaluate their result at compile time if they are called with parameters that are constant throughout the process. This will lead to the fact that later in the binary file will be less code.

If we do not explicitly indicate that lambda expressions are constexpr, but these expressions meet all the required criteria, then they will still be considered constexpr, only implicitly. If it is necessary for the lambda expression to be constexpr, then it is better to explicitly set it as such, since otherwise, in the case of our incorrect actions, the compiler will start generating errors.

exception attr (optional)
Here it is determined whether the function object can throw exceptions if it encounters an error during the call.

return type (optional)
If you need to have full control over the returned type, it is probably not necessary for the compiler to automatically determine it. In such cases, you can simply use the [] () -> Foo {} construct, which will indicate to the compiler that we will always return objects of type Foo.

Add polymorphism by wrapping lambda expressions in std :: function


Suppose you need to write an observer function for some value that may change from time to time, which will alert other objects, such as gas pressure indicator, share prices, etc. When changing the value, a list of observer objects should be called then in their own way they will respond.

To implement the task, you can put several objects of the observer function into a vector, all of them will take as a parameter a variable of type int, which represents the observed value. We do not know what exactly these functions will do when called, but this is not interesting for us.

What type will the function objects have in the vector? We can use the type std :: vector <void (*) (int)> if we capture pointers to functions that have signatures like void f (int) ;. This type will work with any lambda expression that captures something that has a completely different type in comparison with a normal function, since it is not just a pointer to a function, but an object that combines a certain amount of data with a function! Think of the times before C ++ 11, when lambda expressions did not exist. Classes and structures were a natural way of associating data with functions, and changing the types of members of a class will result in a completely different class. It is natural that a vector cannot store values ​​of different types using the same type name.

It is not necessary to indicate to the user that he can save objects to the functions of the observer, which do not capture anything, since this limits application options. How to allow it to save any objects of functions, limiting only the call interface, which accepts a specific range of parameters as observable values?

In this section, we will look at how to solve this problem using the std :: function object, which can act as a polymorphic shell for any lambda expression, no matter what values ​​it captures.

How it's done


In this example, we will create several lambda expressions that are significantly different from each other, but have the same call signature. Then save them in one vector with std :: function.

1. First, turn on the necessary header files:

 #include <iostream> #include <deque> #include <list> #include <vector> #include <functional> 

2. We implement a small function that returns a lambda expression. It takes a container and returns a function object that captures this container by reference. The function object itself takes an integer parameter. When this object receives an integer, it will add it to its container.

 static auto consumer (auto &container){ return [&] (auto value) { container.push_back(value); }; } 

3. Another small helper function will display the contents of the container instance, which we will provide as a parameter:

 static void print (const auto &c) { for (auto i : c) { std::cout << i << ", "; } std::cout << '\n'; } 

4. In the main function, we will create objects of the deque, list and vector classes, each of which will store integers:

 int main() { std::deque<int> d; std::list<int> l; std::vector<int> v; 

5. Now we will use the consumer function for working with our d, l and v container instances: create consumer function objects for them and place them into the vector instance. These function objects will capture a link to one of the container objects. The latter have different types, like function objects. However, the vector stores instances of type std :: function <void (int)>. All function objects implicitly turn into objects of type std :: function, which are then stored in a vector:

 const std::vector<std::function<void(int)>> consumers {consumer(d), consumer(l), consumer(v)}; 

6. Now put ten integer values ​​in all data structures, traversing the values ​​in the loop, and then looping through the objects of the consumer functions that we call with the recorded values:

 for (size_t i {0}; i < 10; ++i) { for (auto &&consume : consumers) { consume(i); } } 

7. All three containers should now contain the same ten numbers. Let's display their contents:

  print(d); print(l); print(v); } 

8. Compiling and running the program will give the following result, which looks exactly as we expected:

 $ ./std_function 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 


»More information about the book can be found on the publisher's website.
» Table of Contents
» Excerpt

For Habrozhiteley a 25% discount on the coupon - C ++ 17 STL

Source: https://habr.com/ru/post/353404/


All Articles