Usage of lambda expression in C + +

Time:2022-5-7

To explain, I use gcc7 1.0 compiler, standard library source code is also this version.

This article explains the usage of lambda expressions in C + + 11.

When I first came into contact with the keyword lambda, I remember it was still in Python, but in fact, we had this keyword in C + + as early as when C + + 11 was launched in 2011. Lambda expression is a new technology introduced in C + + 11. Using lambda expression, we can write embedded anonymous functions to replace independent functions or function objects, and make the code more readable.

The so-called function object is actually a behavior generated by overloading the operator (). For example, we can overload the function in the class and call the operator (). At this time, the class object can directly use () to pass parameters like a function. This behavior is called a function object. Similarly, it is also called an imitation function.

In a broad sense, lambda expression is also a function object, because it is also called directly using () to pass parameters.

1 lambda expressions are basically used

The basic syntax of lambda expression is as follows:

[capture] (formal parameter) - > RET {function body};

Lambda expressions generally start with square brackets [], use () if there are parameters, omit () if there are no parameters, and end at {}, where RET represents the return type.

Let’s take a look at a simple example to define a lambda expression that can output a string. The complete code is as follows:

#include <iostream>

int main()
{
    auto atLambda = [] {std::cout << "hello world" << std::endl;};
    atLambda();
    return 0;
}

The simplest lambda expression defined above has no parameters. If you need parameters, you should put them in parentheses like a function. If there is a return value, the return type should be placed after – > that is, the trailing return type. Of course, you can also ignore the return type. Lambda will automatically deduce the return type for you. Here is a more complex example:

#include <iostream>

int main()
{
    auto print = [](int s) {std::cout << "value is " << s << std::endl;};
    auto lambAdd = [](int a, int b) ->int { return a + b;};
    int iSum = lambAdd(10, 11);
    print(iSum);

    return 0;
}

Lambadd has two input parameters a and B, and its return type is int. we can try it->intRemove, the result is the same.

2 lambda capture block
Simple use of capture 1.2

In Section 1, we showed the syntax form of lambda. The following formal parameters and function body are easy to understand. What does capture mean in square brackets?

In fact, an important concept of lambda expression is closure.

Here, we need to explain the implementation principle of lambda expression: when we define a lambda expression, the compiler will automatically generate an anonymous class, which will implement a public type operator () function by default, which is called closure type. At run time, the lambda expression will return an anonymous closure instance, which is an R-value.

Therefore, the result of our lambda expression above is closure one by one. A powerful feature of closures is that they can capture variables within their encapsulation scope by passing values or references. The square brackets in front are used to define capture patterns and variables, so the part enclosed by square brackets [] is called capture block.

Look at this example:

#include <iostream>

int main()
{
    int x = 10;
    auto print = [](int s) {std::cout << "value is " << s << std::endl;};
    auto lambAdd = [x](int a) { return a + x;};
    auto lambAdd2 = [&x](int a, int b) { return a + b + x;};
    auto iSum = lambAdd(10);
    auto iSum2 = lambAdd2(10, 11);
    print(iSum);
    print(iSum2);

    return 0;
}

When the lambda block is empty, it means that no variables are captured. When it is not empty, for example, lambadd above captures variable x in the form of replication, while lambadd2 captures X by reference. So how does this copy or reference reflect? Let’s use GDB to see the specific types of lambadd and lambadd2, as follows:

(gdb) ptype lambAdd
type = struct <lambda(int)> {
    int __x;
}
(gdb) ptype lambAdd2
type = struct <lambda(int, int)> {
    int &__x;
}
(gdb)

As we said earlier, lambda is actually a class, which has been proved here. In C + +, struct and class are the same except for a few differences. Therefore, we can see that the copy form capture is actually a struct containing int type member variables, and the reference form capture is actually a struct containing int type member variables. Then when running, The member variables will be initialized with the data we captured.

Since there is initialization, there must be a constructor, and then capture the generated member variables and the operator () function. For the time being, a three-dimensional closure type exists in our mind. For the specific composition of lambda expression types, let’s put it aside for the time being and then talk about capture.

2.2 types of capture

The capture method can be reference or copy, but what types of capture are there?

The capture types are as follows:

  • []: no variables are captured by default;
  • []: all variables are captured by copying by default;
  • [&]: capture all variables by reference by default;
  • [x] : capture x only by copying, and other variables are not captured;
  • [x…]: Copy the capture parameters and package variables in the way of package expansion;
  • [& x]: capture x only by reference, and other variables are not captured;
  • [&x…]: Reference capture parameter package variables in package expansion mode;
  • [=, & x]: all variables are captured by copy by default, but x is the exception, which is captured by reference;
  • [&, x]: all variables are captured by reference by default, but x is the exception, which is captured by copying;
  • [this]: capture the current object by reference (actually a copy pointer);
  • [* this]: capture the current object by copying;

It can be seen that lambda can have multiple captures, each separated by commas. In addition, no matter how many capture types, they can’t change, either by copy or by reference.

So what’s the difference between copy capture and reference capture?

Standard C + + stipulates that by default, the overload of operator () in lambda expression is const attribute, which means that variables captured in the form of copy are not allowed to be modified. See this Code:

#include <iostream>

int main()
{
    int x = 10;
    int y = 20;
    auto print = [](int s) {std::cout << "value is " << s << std::endl;};
    auto lambAdd = [x](int a) { 
    //    x++;   Here x is read-only, and self increment is not allowed. An error will be reported during compilation
        return a + x;
    };
    auto lambAdd2 = [&x](int a, int b) { 
        x = x+5;
        return a + b + x;
    };
    auto iSum = lambAdd(10);
    auto iSum2 = lambAdd2(10, 11);
    print(iSum);
    print(iSum2);

    return 0;
}

It can be seen from the code that copy capture does not allow the modification of variable values, while reference capture allows the modification of variable values. Why? Here I understand that & X is actually an int * pointer, so we can modify the value of X, because we only modify the content pointed by the pointer, not the pointer itself, and it is the same as the reference type input declared by us, The modified value is also valid outside the lambda expression.

Then, if I want to use copy capture and modify the value of the variable, we will remember that there is a keyword called mutable, which allows the value of the member variable to be modified in the constant member function, so we can specify the mutable keyword for the lambda expression, as follows:

#include <iostream>

int main()
{
    int x = 10;
    int y = 20;
    auto print = [](int s) {std::cout << "value is " << s << std::endl;};
    auto lambAdd = [x](int a) mutable { 
        x++;
        return a + x;
    };
    auto iSum = lambAdd(10);
    print(iSum);
    print(x);

    return 0;
}

The results are as follows:

value is 21
value is 10

Therefore, after adding mutable, you can modify the copy capture, but one thing is that its modified lambda expression is invalid.

2.3 package deployment method capture

Take a closer look at the capture types in Section 2.2 and you will find [x…] This type actually captures a variable parameter by copying. In C + +, it actually involves the template parameter package, that is, the variable parameter template. See the following example:

#include <iostream>

void tprintf()
{
    return;
}

template<typename U, typename ...Ts>
void tprintf(U u, Ts... ts)
{
    auto t = [ts...]{
        tprintf(ts...);
    };
    std::cout << "value is " << u << std::endl;
    t();
    return;
}

int main()
{
    tprintf(1,'c',3, 8);
    return 0;
}

It captures a set of variable parameters, but this is actually to demonstrate the capture of variable parameters. Lambda expression is forcibly used. If it is not used, the code may be more concise. We only need to know how to use it through this demonstration. In addition, the use of variable parameter template will not be expanded here.

2.4 role of capture

When I look at the capture of lambda, it’s always strange. At first glance, what’s the difference between this capture and passing parameters? It is to pass a variable value into the lambda expression body for use, but if you think carefully, it works. Suppose there is a case where a company has 999 employees, and each employee’s job number is from 1 to 999. Now we want to find out all employees whose job number is an integer multiple of 8, A feasible code is as follows:

#include <iostream>
#include <array>

int main()
{
    int x = 8;
    auto t = [x](int i){
        if ( i % x == 0 )
        {
            std::cout << "value is " << i << std::endl;
        }
    };
    auto t2 = [](int i, int x){
        if ( i % x == 0 )
        {
            std::cout << "value is " << i << std::endl;
        }
    };
    for(int j = 1; j< 1000; j++)
    {
        t(j);
        t2(j, x);
    }
    return 0;
}

Expression t uses capture, but expression T2 does not use capture. In terms of code function and quantity, they are not very different. However, for expression T, the value of X is copied only once, while for expression T2, a temporary variable is generated for each call to store the value of X, which is actually an overhead of time and space. However, for this code, this consumption is negligible, But once the data is on the scale, there will be a big difference.

For the role of capture, I only think of this for the time being. If there are leaders who know more about the role, please say it.

For capture, try not to use the form of full capture such as [=] or [&], because it is uncontrollable. You can’t ensure which variables will be captured, which is prone to some unexpected behavior.

3 lambda expression as callback function

A more important application of lambda expression is that it can be passed in as a function parameter. In this way, the callback function can be realized. For example, in the STL algorithm, some template classes or template functions are often given to specify a template parameter as a lambda expression. As mentioned in the previous section, I want to count the number of employees whose job number is an integer multiple of 8 among 999 employees. An available code is as follows:

#include <iostream>
#include <array>
#include <algorithm>

int main()
{
    int x = 8;
    std::array<int, 999> arr;
    for (int i =1; i< 1000; i++)
    {
        arr[i] = i;
    }
    int cnt = std::count_if(arr.begin(), arr.end(), [x](int a){ return a%x == 0;});
    std::cout << "cnt=" << cnt << std::endl;
    return 0;
}

It is obvious here that we specify a lambda expression as a condition. More often, when using sorting functions, we specify sorting criteria or use lambda expressions.

4 lambda expression assignment

Since a lambda expression generates a class object, can it be assigned as an ordinary class object?

Let’s write a piece of code to try:

#include <iostream>
using namespace std;

int main()
{
    auto a = [] { cout << "A" << endl; };
    auto b = [] { cout << "B" << endl; };
 
    //a = b; //  Illegal, lambda cannot be assigned
    auto c(a); //  Legal, make a copy
    return 0;
}

Obviously, assignment is not allowed, while copy can. Combined with the compiler’s automatic generation of constructor rules, it is obvious that the assignment function is disabled, while the copy constructor is not disabled. Therefore, one lambda expression cannot be used to assign a value to another, but an initial copy can be made.

5 Summary

In a word, according to a definition of lambda expression, it is actually used to replace some functions that are relatively simple but widely used. Lambda is widely used in STL. For most STL algorithms, lambda expression can be used flexibly to achieve the desired effect.

At the same time, it should be explained here that lambda is actually a new syntax rule introduced by C + + 11. It is not directly related to STL, but lambda expressions are widely used in STL, which can not be directly regarded as a part of STL.
Usage of lambda expression in C + +