[C++] A detailed explanation of the usage and development of lambda expressions

1. lambda expression syntax

Lambda expression is a basic feature of modern programming languages, such as LISP, Python, C#, etc. have this feature. But unfortunately, until the C++11 standard, C++ did not support lambda expressions at the language feature level. Programmers have tried to use libraries to implement the function of lambda expressions, such as Boost.Bind or Boost.Lambda, but they have common shortcomings. The implementation code is very complicated, and you need to be very careful when using it. Once an error occurs, it may There will be a bunch of errors and warning messages, and the programming experience is not good in short.

In addition, although C++ has never supported lambda expressions, its demand for lambda expressions is very high. The most obvious is STL. In STL, there are a large number of algorithm functions that need to pass in predicates, such as std::find_if, std::replace_if, etc. In the past there were two ways to implement predicate functions: writing pure functions or functors. However, none of their definitions can be directly applied to the actual parameters of function calls. In the face of complex engineering codes, we may need to switch source files around to search for these functions or functors.

In order to solve the above problems, the C++11 standard provides us with support for lambda expressions, and the syntax is very simple and clear. This simplicity may make us feel that it is a bit out of place with traditional C++ syntax. But after getting used to the new syntax, you will find the convenience of lambda expressions.

The syntax of lambda expressions is very simple, and the specific definitions are as follows:

[ captures ] ( params ) specifiers exception -> ret { body }
  • [ captures ] —— a capture list, which can capture zero or more variables in the current function scope, and the variables are separated by commas. In the corresponding example, [x] is a capture list, but it only captures a variable x in the current function scope. After capturing the variable, we can use this variable in the body of the lambda expression function, such as return x * y. In addition, there are two ways to capture a capture list: capture by value and capture by reference, which will be described in detail below.
  • ( params ) —— The optional parameter list, the syntax is the same as the parameter list of ordinary functions, and the parameter list can be ignored when no parameters are needed. Corresponds to (int y) in the example.
  • specifiers - optional qualifiers, mutable can be used in C++11, which allows us to change variables captured by value in the lambda expression function body, or call non-const member functions. No specifier is used in the above example.
  • exception - Optional exception specifier, we can use noexcept to indicate whether the lambda will throw an exception. The corresponding examples do not use exception specifiers.
  • ret —— Optional return value type. Different from ordinary functions, lambda expressions use the syntax behind the return type to indicate the return type. If there is no return value (void type), the entire part including -> can be ignored. In addition, we can also not specify the return type when there is a return value, and the compiler will deduce a return type for us. Corresponding to the above example is ->int.
  • { body } —— the function body of the lambda expression, this part is the same as the function body of a normal function. Corresponds to { return x * y; } in the example.

Since the parameter list, qualifiers, and return value are all optional, the simplest lambda expression we can write is:

[]{}

Although it looks very strange, it is indeed a legal lambda expression. It needs to be emphasized that the above grammar definition only belongs to the C++11 standard, and the C++14 and C++17 standards have made useful extensions to lambda expressions, which will be introduced later.

2. Capture list

In the grammar of lambda expressions, the most different part from the traditional C++ grammar should be regarded as the capture list. In fact, it is also the most complicated part of the lambda expression, in addition to the large syntax difference. Next, we will break down the capture list to discuss its characteristics step by step.

2.1 Scope

We must understand the scope of the capture list. Usually we say that an object is in a certain scope, but this statement has changed in the capture list.

Variables in a capture list exist in two scopes - the scope of the function defined by the lambda expression and the scope of the function body of the lambda expression. The former is for capturing variables, the latter is for using variables. In addition, the standard also stipulates that the variable that can be captured must be an automatic storage type. Simply put, it is a non-static local variable.

Let's take a look at the following example:

int x = 0;
int main()
{
int y = 0;
static int z = 0;
auto foo = [x, y, z] {};
}

The above code may not be able to be compiled (why is it possible? Because the compilers of different manufacturers may handle differently, for example, GCC will not report an error, but will give a warning). There are two reasons: first, the variable x and z are not variables of automatic storage type; second, x does not exist in the scope defined by the lambda expression. So what if you want to use global variables or static local variables in lambda expressions? The solution that comes to mind right away is to use the parameter list to pass global variables or static local variables. In fact, it doesn’t have to be so troublesome, just use it directly. Let’s take a look at the following code:

#include <iostream>
int x = 1;
int main()
{
int y = 2;
static int z = 3;
auto foo = [y] { return x + y + z; };
std::cout << foo() << std::endl;
}

In the above code, although we did not capture the variables x and z, we can still use them. Furthermore, if we define a lambda expression in the global scope, then the capture list of the lambda expression must be empty. Because according to the rules mentioned above, the variable of the capture list must be an automatic storage type, but the global scope does not have such a type, such as:

int x = 1;
auto foo = [] { return x; };
int main()
{
foo();
}

2.2 Capture value and capture reference

The capture method of the capture list is divided into capture value and capture reference. We have seen the syntax of capture value in the previous example. Write the variable name directly in [], and if there are multiple variables, separate them with commas, for example :

int main()
{
int x = 5, y = 8;
auto foo = [x, y] { return x * y; };
}

The capture value is to copy the values ​​of x and y in the function scope to the interior of the lambda expression object, just like the member variables of the lambda expression.

There is only one & difference between the capture reference syntax and the capture value. To express a capture reference, we only need to add & before the capture variable, which is similar to taking a variable pointer. It's just that what is captured here is a reference instead of a pointer. In a lambda expression, you can directly use the variable name to access the variable without dereferencing, for example:

int main()
{
int x = 5, y = 8;
auto foo = [&x, &y] { return x * y; };
}

The above two examples just read the value of the variable. From the results, there is no difference between the two captures, but if the assignment operation of the variable is added, the situation is different. Please see the following example:

void bar1()
{
int x = 5, y = 8;
auto foo = [x, y] {
x += 1; // 编译失败,无法改变捕获变量的值
y += 2; // 编译失败,无法改变捕获变量的值
return x * y;
};
std::cout << foo() << std::endl;
}
void bar2()
{
int x = 5, y = 8;
auto foo = [&x, &y] {
x += 1;
y += 2;
return x * y;
};
std::cout << foo() << std::endl;
}

In the code above the function bar1 fails to compile because we cannot change the value of the captured variable. This leads to a feature of lambda expressions: the captured variable defaults to a constant , or lambda is a constant function (similar to a constant member function).

The lambda expression in the bar2 function can be successfully compiled, although the function body also has the behavior of changing the variables x and y. This is because the captured variable defaults to a constant and refers to the variable itself. When the variable is captured by value, the variable itself is the value, so changing the value will cause an error. On the contrary, in the case of capturing references, the captured variable is actually a reference. What we change in the function body is not the reference itself, but the value of the reference, so it is not rejected by the compiler.

Also, remember the optional specifier mutable mentioned above? Using the mutable specifier can remove the constness of the lambda expression, which means that we can modify the variables that capture the value in the function body of the lambda expression, for example:

void bar3()
{
int x = 5, y = 8;
auto foo = [x, y] () mutable {
x += 1;
y += 2;
return x * y;
};
std::cout << foo() << std::endl;
}

The above code can be compiled, which means that the lambda expression successfully modifies the values ​​of x and y within its scope. It is worth noting that, compared to function bar1, function bar3 has an additional pair of () in addition to the specifier mutable. This is because the grammar stipulates that if there is a specifier in the lambda expression, the formal parameter list cannot be omitted.

Compiling and running the two functions bar2 and bar3 will output the same results, but this does not mean that the two functions are equivalent, and there is still an essential difference between capturing values ​​and capturing references.

When a lambda expression captures a value, what is actually obtained in the expression is a copy of the captured variable. We can modify the internal captured variable arbitrarily, but it will not affect the external variable. However, capturing references is different. Modifying the variable captured in the lambda expression will also modify the corresponding external variable:

#include <iostream>
int main()
{
int x = 5, y = 8;
auto foo = [x, &y]() mutable {
x += 1;
y += 2;
std::cout << "lambda x = " << x << ", y = " << y <<
std::endl;
return x * y;
};
foo();
std::cout << "call1 x = " << x << ", y = " << y << std::endl;
foo();
std::cout << "call2 x = " << x << ", y = " << y << std::endl;
}

The result of the operation is as follows:

lambda x = 6, y = 10
call1 x = 5, y = 10
lambda x = 7, y = 12
call2 x = 5, y = 12

There is one more thing to note about the lambda expression that captures the value. The variable that captures the value has been fixed when the lambda expression is defined. No matter how the function modifies the value of the external variable after the lambda expression is defined, the value captured by the lambda expression Neither will change.

#include <iostream>
int main()
{
    int  x = 5, y = 8;
    auto foo = [x, &y]() mutable
    {
        x += 1;
        y += 2;
        std::cout << "lambda x = " << x << ", y = " << y << std::endl;
        return x * y;
    };
    x = 9;
    y = 20;
    foo();
}

The result of the operation is as follows:

lambda x = 6, y = 22

In the above code, although the values ​​of x and y are respectively modified before calling foo, the variable x that captures the value still continues the value when the lambda was defined, and after the variable y that captures the reference is reassigned, the lambda expression The value of the captured variable y also changes accordingly.

2.3 Special capture methods

In addition to specifying capture variables, the capture list of lambda expressions has three special capture methods.

  1. [this] —— Capture this pointer, capturing this pointer allows us to use member variables and functions of this type.
  2. [=] —— captures the values ​​of all variables in the scope defined by the lambda expression, including this.
  3. [&] —— captures references to all variables in the scope defined by the lambda expression, including this.

First, let's take a look at the situation of capturing this:

#include <iostream>
class A
{
public:
    void print()
    {
        std::cout << "class A" << std::endl;
    }
    void test()
    {
        auto foo = [this]
        {
            print();
            x = 5;
        };
        foo();
    }

private:
    int x;
};
int main()
{
    A a;
    a.test();
}

In the above code, because the lambda expression captures the this pointer, the member function print of this type can be called within the lambda expression or its member variable x can be used.

It is easier to understand by capturing the value or reference of all variables:

#include <iostream>
int main()
{
int x = 5, y = 8;
auto foo = [=] { return x * y; };
std::cout << foo() << std::endl;
}

3. Implementation principle of lambda expression

If you are a C++ veteran, you may have found that lambda expressions are very similar to function objects (functors), so let's start with function objects and explore the implementation principles of lambda expressions in depth. See the example below:

#include <iostream>
class Bar
{
public:
    Bar(int x, int y) : x_(x), y_(y) {}
    int operator()()
    {
        return x_ * y_;
    }

private:
    int x_;
    int y_;
};
int main()
{
    int  x = 5, y = 8;
    auto foo = [x, y]
    {
        return x * y;
    };
    Bar bar(x, y);
    std::cout << "foo() = " << foo() << std::endl;
    std::cout << "bar() = " << bar() << std::endl;
}

In the above code, foo is a lambda expression and bar is a function object. They can all get the values ​​of the variables x and y in the main function at the time of initialization, and return the same result after calling. The more obvious differences between the two are as follows:

  1. Using lambda expressions does not require us to explicitly define a class, which has a great advantage in quickly implementing functions.
  2. Using function objects can have richer operations during initialization, such as Bar bar(x+y, x * y), and this operation is not allowed in the lambda expression of the C++11 standard (C++14 It was later extended to allow this). In addition, it is no problem to use global or static local variables when Bar initializes objects.

It seems that in the C++11 standard, the advantage of lambda expressions is that they are easy to write and easy to maintain, while the advantage of function objects is that they are more flexible and unrestricted, but in general they are very similar. In fact, this is exactly how lambda expressions are implemented.

The lambda expression will automatically generate a closure class by the compiler at compile time, and an object will be generated from this closure class at runtime, we call it a closure. In C++, the so-called closure can be simply understood as an anonymous function object that can contain the scope context at the time of definition. Now let's put these concepts aside and see what a lambda expression actually looks like.

First, define a simple lambda expression:

#include <iostream>
int main()
{
int x = 5, y = 8;
auto foo = [=] { return x * y; };
int z = foo();
}

Next, we use GCC to output its GIMPLE intermediate code:

main()
{
    int D .39253;
    {
        int                      x;
        int                      y;
        struct __lambda0         foo;
        typedef struct __lambda0 __lambda0;
        int                      z;
        try
        {
            x       = 5;
            y       = 8;
            foo.__x = x;
            foo.__y = y;
            z       = main()::<lambda()>::operator()(&foo);
        }
        finally
        {
            foo = { CLOBBER };
        }
    }
    D .39253 = 0;
    return D .39253;
}
main()::<lambda()>::operator()(const struct __lambda0* const __closure)
{
    int       D .39255;
    const int x [value - expr:__closure->__x];
    const int y [value - expr:__closure->__y];
    _1       = __closure->__x;
    _2       = __closure->__y;
    D .39255 = _1 * _2;
    return D .39255;
}

From the above intermediate code, we can see that the type of lambda expression is named __lambda0, the object foo is instantiated through this type, and then the members __x and __y of the foo object are assigned in the function, and finally through the custom ( ) operator performs a calculation on the expression and assigns the result to the variable z. In this process, __lambda0 is a structure with operator() custom operator, which is exactly the characteristic of the function object type. Therefore, to a certain extent, lambda expressions are just a piece of grammatical sugar provided by C++11. The functions of lambda expressions can be implemented manually, and if the implementation is reasonable, the code will not be efficient in operation. The gap is just that the practical lambda expression makes code writing easier.

4. Stateless lambda expressions

The C++ standard has special care for stateless lambda expressions, that is, it can be implicitly converted to a function pointer, for example:

void f(void (*)()) {}
void g()
{
    f([] {});
}   // 编译成功

In the above code, the lambda expression []{} is implicitly converted to a function pointer of type void(*)(). Similarly, look at the following code:

void f(void (&)()) {}
void g()
{
    f(*[] {});
}

This code can also be successfully compiled. We often encounter this application of lambda expressions in STL code.

5. Using lambda expressions in STL

To discuss the common use of lambda expressions, it is necessary to discuss the C++ standard library STL. In STL, we often see some algorithm functions whose formal parameters need to pass in a function pointer or function object to complete the entire algorithm, such as std::sort, std::find_if, etc.

Before the C++11 standard, we usually needed to define a helper function or helper function object type outside the function. For simple needs, we may also use auxiliary functions provided by STL, such as std::less, std::plus, etc. In addition, functions such as std::bind1st and std::bind2nd may also be used for slightly more complex requirements. In short, no matter which of the above methods is used, the expression is quite obscure. Most of the time, we will probably have to write helper functions or helper function object types ourselves.

Fortunately, with lambda expressions, these problems are easily solved. We can implement helper functions directly inside the parameter list of the STL algorithm function, for example:

#include <iostream>
#include <vector>
#include <algorithm>
int main()
{
std::vector<int> x = {1, 2, 3, 4, 5};
std::cout << *std::find_if(x.cbegin(),
x.cend(),
[](int i) { return (i % 3) == 0; }) <<
std::endl;
}

The function std::find_if needs an auxiliary function to help determine the value to be found, and here we use lambda expressions to define the auxiliary function directly when passing parameters. Whether writing or reading code, defining lambda expressions directly is more concise and easier to understand than defining helper functions.

6. Generalized capture

Generalized capture is defined in the C++14 standard. The so-called generalized capture is actually two capture methods. The first is called simple capture. This capture is the capture method we mentioned above, namely [identifier], [ &identifier] and [this] etc. The second is called initialization capture. This capture method was introduced in the C++14 standard. It solves an important problem of simple capture, that is, it can only capture the variables of the lambda expression definition context, but cannot capture the expression Results and custom capture variable names, such as:

int main()
{
int x = 5;
auto foo = [x = x + 1]{ return x; };
}

The above cannot be compiled before the C++14 standard, because the C++11 standard only supports simple capture. The C++14 standard supports such captures. In this code, the capture list is an assignment expression, but this assignment expression is a bit special because it spans two scopes through the equal sign. The variable x on the left side of the equal sign exists in the scope of the lambda expression, while the variable x on the right side of the equal sign exists in the scope of the main function. If readers feel that the writing of the two x's is a bit convoluted, we can also use a clearer way of writing:

int main()
{
int x = 5;
auto foo = [r = x + 1]{ return r; };
}

Obviously, the variable r here only exists in the lambda expression. If the variable x is used in the lambda expression function body at this time, a compilation error will occur. Initialization capture is very practical in some scenarios. Here are two examples. The first scenario is to use move operations to reduce the overhead of code execution, for example:

#include <string>
int main()
{
std::string x = "hello c++ ";
auto foo = [x = std::move(x)]{ return x + "world"; };
}

The above code uses std::move to initialize the capture list variable x, which avoids the copy assignment operation of the object, thereby improving the code efficiency.

The second scenario is to copy the this object during an asynchronous call to prevent undefined behavior due to the destruction of the original this object when the lambda expression is called, such as:

#include <iostream>
#include <future>
class Work
{
private:
int value;
public:
Work() : value(42) {}
std::future<int> spawn()
{
return std::async([=]() -> int { return value; });
}
};
std::future<int> foo()
{
Work tmp;
return tmp.spawn();
}
int main()
{
std::future<int> f = foo();
f.wait();
std::cout << "f.get() = " << f.get() << std::endl;
}

The output is as follows:

f.get() = 32766

Here we expect the result returned by f.get() to be 42, but actually returns 32766, which is an undefined behavior, which causes the calculation error of the program, and may even cause the program to crash. To solve this problem, we introduce the feature of initialization capture to copy the object into the lambda expression, let's simply modify the spawn function:

class Work
{
private:
int value;
public:
Work() : value(42) {}
std::future<int> spawn()
{
return std::async([=, tmp = *this]() -> int { return
tmp.value; });
}
};

The above code uses initialization capture, copies *this to the tmp object, and then returns the value of the tmp object in the function body. Since the entire object is passed into the lambda expression by copying, even if the object pointed to by this is destructed, it will not affect the calculation of the lambda expression. Compile and run the modified code, and the program correctly outputs f.get() = 42.

7. Generic lambda expressions

The C++14 standard enables lambda expressions to have the ability to template functions, which we call generic lambda expressions. Although it has the ability of template functions, its definition method does not use the template keyword. In fact, the generic lambda expression syntax is much simpler, we only need to use the auto placeholder, for example:

int main()
{
auto foo = [](auto a) { return a; };
int three = foo(3);
char const* hello = foo("hello");
}

8. Constant lambda expressions and capturing *this

The C++17 standard also has two enhancements to lambda expressions, one is a constant lambda expression, and the other is an enhancement to capturing *this. Here we mainly explain the enhancement of capturing this. Remember the code that initialized the captured *this object earlier? We copy the object pointed to by this to tmp in the capture list, and then use the value of tmp. Yes, this does solve the asynchronous problem, but the solution is not elegant. Just imagine, if a large number of objects pointed to by this are used in lambda expressions, then we have to modify them all, and any omission will cause problems. In order to copy and use the *this object more conveniently, C++17 adds the syntax of the capture list to simplify this operation. Specifically, add [*this] directly to the capture list, and then use it directly in the body of the lambda expression function This points to the object member, or take the previous Work class as an example:

class Work
{
private:
int value;
public:
Work() : value(42) {}
std::future<int> spawn()
{
return std::async([=, *this]() -> int { return value; });
}
};

In the above code, instead of using tmp=*this to initialize the capture list, *this is used directly. In the lambda expression, tmp.value is not used anymore but value is returned directly. Compile and run this code to get the expected result 42. It can be seen from the results that the syntax of [*this] allows the program to generate a copy of the *this object and store it in the lambda expression, and the members of the copied object can be directly accessed in the lambda expression, eliminating the previous lambda expression The awkwardness of needing to access object members via tmp.

9. Capture [=, this]

In the C++20 standard, lambda expressions have been slightly modified. This modification does not strengthen the ability of lambda expressions, but makes the related semantics of this pointer clearer. We know that [=] can capture this pointer, similarly, [=,*this] will capture a copy of this object. But when there are a lot of [=] and [=,*this] in the code, we may easily forget the difference between the former and the latter. In order to solve this problem, the syntax of [=, this] to capture this pointer was introduced in the C++20 standard. It actually expresses the same meaning as [=], and the purpose is to let programmers distinguish it from [=, * this] difference:

[=, this]{}; // C++17 编译报错或者报警告, C++20成功编译

Although [=, this]{}; is considered to have a syntax problem in the C++17 standard, in practice, both GCC and CLang only give warnings without reporting errors. In addition, in the C++20 standard, it is also emphasized that [=, this] should be used instead of [=]. If you compile the following code with GCC:

template <class T>
void g(T) {}
struct Foo {
int n = 0;
void f(int a) {
g([=](int k) { return n + a * k; });
}
};

The compiler will output a warning message, indicating that the standard no longer supports the use of [=] to implicitly capture the this pointer, and prompts the user to explicitly add this or *this. Finally, it is worth noting that it is not allowed to capture the this pointer with two syntaxes at the same time, such as:

[this, *this]{};

This way of writing will definitely give a compilation error in CLang, while GCC will give a slightly gentle warning. In my opinion, this way of writing is meaningless and should be avoided.

10. Generic lambda expressions for template syntax

We discussed how lambda expressions in the C++14 standard implement generics by supporting auto. In most cases, this is a nice feature, but unfortunately, this syntax also makes it difficult for us to interact with the type, and the operation on the type becomes extremely complicated. Using the example of the proposal document:

template <typename T> struct is_std_vector : std::false_type { };
template <typename T> struct is_std_vector<std::vector<T>> :
std::true_type { };
auto f = [](auto vector) {
static_assert(is_std_vector<decltype(vector)>::value, "");
};

Ordinary function templates can easily match a container object whose actual parameter is vector through the formal parameter pattern, but for lambda expressions, auto does not have this expressive ability, so it has to implement is_std_vector, and use static_assert to assist in judging the actual parameter Whether the real type is vector. In the opinion of experts on the C++ committee, it is inappropriate to entrust static_assert with a task that could have been accomplished through template deduction. In addition, such syntax makes it very complicated to obtain the type of vector storage object, for example:

auto f = [](auto vector) {
using T = typename decltype(vector)::value_type;
// …
};

Of course, it is a fluke to be able to achieve this. We know that the vector container type will use the built-in type value_type to represent the type of storage object. But we can't guarantee that all containers we face will implement this rule, so relying on embedded types is unreliable. Furthermore, decltype(obj) sometimes cannot directly obtain the type we want. Readers who don't remember the rules of decltype derivation can review the previous chapters, and here is the sample code directly:

auto f = [](const auto& x) {
using T = decltype(x);
T copy = x; // 可以编译,但是语义错误
using Iterator = typename T::iterator; // 编译错误
};
std::vector<int> v;
f(v);

Please note that in the above code, the type deduced by decltype(x) is not std::vector, but const std::vector &, so T copy = x; is not a copy but a reference. For a reference type, T::iterator is also ungrammatical, so a compilation error occurs. In the proposal document, the author kindly gave a solution. He used the decay of STL, so that the cv and reference attributes of the type can be deleted, so the following code:

auto f = [](const auto& x) {
using T = std::decay_t<decltype(x)>;
T copy = x;
using Iterator = typename T::iterator;
};

Although the problem is solved, we must always pay attention to auto, so as not to bring unexpected problems to the code. Moreover, this can only continue when the container itself is well designed.

In view of the above problems, the C++ committee decided to add template support for lambda in C++20. The syntax is very simple:

[]<typename T>(T t) {}

So, the examples above that confuse us can be rewritten as:

auto f = []<typename T>(std::vector<T> vector) {
// …
};

as well as

auto f = []<typename T>(T const& x) {
T copy = x;
using Iterator = typename T::iterator;
};

Does the above code catch your eye? These codes are not only much more concise, but also more in line with the habits of C++ generic programming.

Finally, let me tell you an interesting story. In fact, as early as 2012, the proposal document N3418 for lambda to support templates had been submitted to the C++ committee, but the proposal was not accepted at that time. Types are implemented in the C++14 standard, and in 2017 the proposal for lambda support for templates was proposed again. This time, it can be said that it has successfully joined the C++20 standard by stepping on the shoulders of N3559. Looking back at the whole process, although it is not tortuous, it is also quite intriguing. As a language that has been developed for nearly 30 years, C++ is still moving forward through continuous exploration and error correction.

11. Constructable and assignable stateless lambda expressions

We mentioned that stateless lambda expressions can be converted to function pointers, but unfortunately, before the C++20 standard, stateless lambda expression types could neither be constructed nor assigned, which hindered the implementation of many applications. For example, we have learned that functions like std::sort and std::find_if need a function object or function pointer to assist sorting and searching, in which case we can use lambda expressions to complete the task. But if you encounter a container type such as std::map, it will be difficult to handle, because the comparison function object of std::map is determined by the template parameter. At this time, what we need is a type:

auto greater = [](auto x, auto y) { return x > y; };
std::map<std::string, int, decltype(greater)> mymap;

The intention of this code is obvious. It first defines a stateless lambda expression greatate, and then uses decltype(greater) to obtain its type and pass it into the template as a template argument. This idea is very good, but it is not feasible in the C++17 standard, because the lambda expression type cannot be constructed. The compiler will clearly inform that the default constructor of the lambda expression has been deleted ("note: a lambda closure type has a deleted default constructor"). In addition to being unable to construct, stateless lambda expressions cannot be assigned, such as:

auto greater = [](auto x, auto y) { return x > y; };
std::map<std::string, int, decltype(greater)> mymap1, mymap2;
mymap1 = mymap2;

Here mymap1 = mymap2; will also be reported by the compiler, because the copy assignment function has also been deleted (“note: a lambda closure type has a deleted copy assignment operator”). In order to solve the above problems, the C++20 standard allows the construction and assignment of stateless lambda expression types, so it is feasible to use the C++20 standard compilation environment to compile the above code.

Summarize

The above introduces the syntax, usage and principle of lambda expressions. In general, lambda expressions are not only easy to use, but also easy to understand the principle. It solves the embarrassment of not being able to directly write inline functions in C++ in the past. Although a C language extension called nest function is provided in GCC, this extension allows us to write nested functions inside functions, but this feature has not been included in the standard. Of course, we don't need to regret this, because the lambda expression provided now is better than nest function in terms of grammatical simplicity and versatility. Reasonable use of lambda expressions can make the code shorter and more readable at the same time.

Guess you like

Origin blog.csdn.net/weixin_43717839/article/details/132637639