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

    imageThe book describes working with containers, algorithms, auxiliary classes, lambda expressions and other interesting tools that modern C ++ is rich in. Having mastered the material, you will be able to radically revise the familiar approach to programming. The advantage of the publication is in the detailed description of the standard C ++, STL template library. Her latest version was released in 2017. In the book you will find more than 90 of the most realistic examples that demonstrate the full power of STL. Many of them will become the 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, applicable in various industries.

    The following is an excerpt from Lambda Expressions.

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

    Lambda expressions or lambda functions create closures. Closing is a very general term for nameless objects that can be called as functions. To provide a similar opportunity in C ++, such an object must implement a function call operator (), with or without parameters. Creating a similar object without lambda expressions before C ++ 11 would have looked like this:

    #include 
    #include 
    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 anonymous, unlike lambda expressions. From the point of view of closures, it can be argued that they capture a string. When the sample instance is called as a function without parameters, the string “Hello, John Doe” is displayed, because we specified a string with that name.

    Starting with C ++ 11, creating such closures has become easier:

    #include 
    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 by a small construction [] {/ * to do something * /}, which at first glance looks strange, but already in the next section we will consider all possible cases of its application.

    Lambda expressions help keep the code generic and clean. They can be used as parameters for generalized algorithms to refine them when processing specific types defined by the user. They can also be used to wrap work packages and data so that they can be run in threads or just save work and delay the execution of packages. Since the advent of C ++ 11, many libraries have been created that work with lambda expressions, since they have become a natural part of the C ++ language. Another use case for lambda expressions is metaprogramming, as they can be evaluated at runtime. However, we will not consider this issue, since it does not relate to the topic of this book.

    In the current chapter, we will largely rely on individual functional programming patterns, which may seem strange to beginners and even experienced programmers who have not yet worked with such patterns. If in the following examples you see lambda expressions that return lambda expressions, which again return lambda expressions, then please do not get lost. We somewhat go beyond the usual programming style to get ready to work with the modern C ++ language, in which functional programming patterns are found more and more. If the code for some example looks too complicated, take the time to take it apart in more detail. Once you figure it out, complex lambda expressions in real projects will no longer bother you.

    Dynamic function definition using lambda expressions


    Using lambda expressions, you can encapsulate the code to call it later or even elsewhere, as it is allowed to be copied. 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 syntax of lambda expressions looked new in C ++ 11, and it changed a bit with C ++ 17. In this section, we will see how lambda expressions now look 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 to understand the basic principles of interacting with them.

    1. To work with lambda expressions, library support is not needed, but we will output messages to the console and use strings, so we will need the corresponding header files:

    #include 
    #include 

    2. In this example, all the action takes place in the main function. We will define two function objects that do not accept parameters, and return integer constants with values ​​1 and 2. Note: the return expression is surrounded by curly braces {}, 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 the square brackets [] must be present:

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

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

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

    Forget about them and define another function object, which is called plus, - it takes two parameters and returns their sum:

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

    5. Using such an object is quite simple; in this regard, it is similar to any other binary function. We indicated that its parameters are of type auto, as a result of which 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 necessary, 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 the closure that contains the integer counter. With each call, the value of this counter will increase by 1 and return a new value. To indicate that the closure contains an internal counter, place the expression count = 0 in parentheses - it indicates that the variable count 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 we call the function object five times and print 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 capture 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 will be available outside of it. To do this, we put the & a construct in brackets, where the & symbol means that we keep a reference to the variable, but not a copy:

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

    10. If this works, then you can call this function object several times, and then observe whether 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 save it in another function object that takes less parameters. In this case, we save the plus function and accept only one parameter, which will be passed to the plus function. Another parameter has a value of 10; we save it in the function object. Thus, we get the function and call it plus_ten, since it can add the value 10 to the only parameter it accepts.

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

    12. Before compiling and starting the program, we will go through the code again and try to predict which values ​​will be displayed in the terminal. Then run the program and take a look at the actual 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 done now does not look too complicated: we added the numbers, and then incremented them and displayed on the screen. We even performed string concatenation using a function object that was implemented to add numbers. But for those still unfamiliar with the syntax of lambda expressions, this may seem confusing.

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

    image

    Generally, you can omit most of these options to save a little time. The shortest lambda expression is the expression [] {}. It does not accept any parameters, does not capture anything and, in fact, does nothing. What does the rest mean?

    Capture list


    Determines what exactly we capture and whether we capture at all. There are several ways to do this. Let's consider two "lazy" options.

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

    2. The entry [&] () {...} 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 notation [a, & b] () {...} means that we capture the variable a by value, and the variable b by reference. To do this, you need to print more text, but, as a rule, this method is safer, since we cannot accidentally capture something unnecessary from outside the closure.

    In the current example, we defined the lambda expression as follows: [count = 0] () {...}. In this special case, we do not capture any variables from outside the closure, we only define a new variable called 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 the link to b;
    • [&, a] () {...} - copy a and use the link to any other passed variable;
    • [=, & b, i {22}, this] () {...} - we get a link to b, copy the value of this, initialize the new variable i with the value 22 and copy any other used variable.

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

    constexpr (optional)
    If we explicitly mark the lambda expression with the constexpr keyword, then 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 result in less code in the binary later.

    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 you want the lambda expression to be constexpr, then it is better to explicitly set it as such, because otherwise, in the case of our incorrect actions, the compiler will start to generate errors.

    exception attr (optional)
    This determines whether the function object can throw exceptions if it encounters an error when called.

    return type (optional)
    If you need to have full control over the return type, you probably don't need the compiler to detect it automatically. In such cases, you can simply use the [] () -> Foo {} construct, which tells the compiler that we will always return objects of type Foo.

    Add polymorphism by wrapping lambda expressions in std :: function


    Suppose you want to write an observer function for some value that can change from time to time, which will lead to notification of other objects, for example, gas pressure indicator, share price, etc. When changing the value, a list of observer objects should be called up, which then they will respond in their own way.

    To implement the task, you can put several objects of the observer function in 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 we are not interested in this.

    What type will function objects placed in a vector have? The type std :: vector is suitable for usif 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 compared to a regular function, since this is not just a pointer to a function, but an object that combines a certain amount of data with a function! Think about the times before C ++ 11 came about when lambda expressions did not exist. Classes and structures were a natural way of associating data with functions, and changing the types of class members will result in a completely different class. It is natural that a vector cannot store values ​​of different types using the same type name.

    You should not tell the user that he can save objects of the observer function that do not capture anything, since this limits the application. How to allow it to save any function objects, limiting only the call interface, which takes a specific range of parameters in the form of 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, regardless of 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 using std :: function.

    1. First, include the necessary header files:

    #include 
    #include 
    #include 
    #include 
    #include 

    2. We implement a small function that returns a lambda expression. It takes a container and returns a function object that captures that 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 d;
         std::list l;
         std::vector v;

    5. Now we will use the consumer function to work with our container instances d, l and v: we will create function consumer objects for them and put them in 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. All function objects are implicitly wrapped in objects of type std :: function, which are then stored in the vector:

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

    6. Now we place ten integer values ​​in all data structures, passing through the values ​​in the loop, and then we go through the loop through the objects of the consumer functions, which 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. Display their contents on the screen:

       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 Khabrozhiteley 25% discount on coupon - С ++ 17 STL

    Also popular now: