To clarify, I am using the GCC7.1.0 compiler, and the standard library source code is also in this version.

This article explains the use of lambda expressions in c++11.

Lambda was first introduced to c++ when c++11 was released in 2011. Lambda expressions are a new technique introduced in C++11 that allows you to write embedded anonymous functions to replace stand-alone functions or function objects and make your code more readable.

A function object is a behavior that results from overloading operator(). For example, if we call the operator() in a class by overloading the function, the class object can use () to pass arguments just like a function. This behavior is called a function object.

Lambda expressions are also a function object in a broad sense, because they are called directly with () as arguments.

Basic use of lambda expressions

The basic syntax for lambda expressions is as follows:

[capture] (parameter) -> ret {function body};Copy the code

Lambda expressions generally start with square brackets [], use () if there are arguments, omit () if there are no arguments, and end with {}, where ret indicates the return type.

Let’s start with a simple example. Define a lambda expression that outputs a string. The complete code looks like this:

#include <iostream>

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

The simplest lambda expression, with no arguments, is defined above. If you need an argument, place it in parentheses, like a function. If there is a return value, place the return type after ->, which is the trailing return type. You can also ignore the return type. Lambda will automatically deduce the return type for you.

#include <iostream>

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

	return 0;
}
Copy the code

LambAdd has two input parameters a and b, and its return type is int. We can try to remove ->int, and the result is the same.

2 Lambda capture block
2.1 Simple use of capture

In Section 1, we showed the syntax of lambda. The parameters and function body are easy to understand, but what does it mean to capture in square brackets?

There is an important concept involved in lambda expressions, and that is closures.

When we define a lambda expression, the compiler automatically generates an anonymous class that, by default, implements an operator() function of type public, called the closure type. At run time, the lambda expression returns an anonymous instance of the closure, which is an rvalue.

So, the result of our lambda expression above is closure by closure. One of the strengths of closures is that they can capture variables in their enclosing scope by passing values or references. The preceding square brackets are used to define capture modes and variables, so we call the parts enclosed in square brackets [] as capture blocks.

Look at this example:

#include <iostream>

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

	return 0;
}
Copy the code

When lambda blocks are empty, no variables are captured. When lambda blocks are not empty, for example, lambAdd above captures variable X as a copy, while lambAdd2 captures x as a reference. 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)
Copy the code

We said lambda is actually a class, and it turns out that in c++ struct and class are the same except for a few minor differences, so we can see that the copy form capture is actually a struct that contains a member variable of type int, The referential capture is actually a struct that contains a member variable of type int&, and then at run time, the member variable is initialized with the data we captured.

Since there are initializers, there must be constructors that capture generated member variables, and operator() functions. For the moment, a three-dimensional closure type has been in mind.

2.2 Types of capture

The capture can be referenced or copied, but what types of capture are there?

The capture type is as follows:

  • [] : no variables are captured by default;
  • [=] : all variables are captured by copy by default;
  • [&] : Captures all variables by reference by default;
  • [x] : capture x only by copy, other variables are not captured;
  • [x…] : copy capture parameter package variables in package expansion mode;
  • [&x] : capture x by reference only, other variables are not captured;
  • [&x…] : capture parameter package variables by reference in package expansion mode;
  • [=, &x] : All variables are captured by copy by default, except x, which is captured by reference;
  • [&, x] : All variables are captured by reference by default, with the exception of x, which is captured by copy;
  • [this] : Capture current object by reference (actually copy pointer);
  • [*this] : capture current object by copy;

As you can see, lambdas can have multiple captures, each separated by a comma, and no matter how many capture types there are, they can be captured either by copy or by reference.

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

Standard c++ dictates that, by default, overloading operator() in lambda expressions is const, which means that variables captured as copies are not allowed to be modified.

#include <iostream>

int main(a)
{
	int x = 10;
	int y = 20;
	auto print = [](int s) {std::cout << "value is "<< s << std::endl; };auto lambAdd = [x](int a) { 
	// x++; X is read-only and cannot be incremented
		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;
}
Copy the code

Can be seen from the code, copy the capture is not allowed to modify the variable values, and references to capture is allowed to modify the variable values, why, I understand, here & x is actually a pointer of type int *, so we can modify the value of x, because we just modify the contents of this pointer is pointing to, not to modify the pointer itself, And as with our regular declaration of reference type inputs, the modified value is valid outside of a lambda expression.

If I want to capture by copy and change the value of a variable, then I remember that the mutable keyword is mutable, which allows to change the value of a member variable in a constant member function. So we can specify the mutable keyword for lambda expressions as follows:

#include <iostream>

int main(a)
{
	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;
}
Copy the code

The result is as follows:

value is 21
value is 10
Copy the code

So it is possible to modify copy capture by adding it to be mutable, but its modification is invalid outside of a lambda expression.

2.3 Packet Expansion Mode Capture

If you look closely at the captured types in Section 2.2, you’ll see [x…] This type actually captures a variable argument by copying it. In c++, it actually involves the template parameter package, also known as the variable parameter template. See the following example:

#include <iostream>

void tprintf(a)
{
	return;
}

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

int main(a)
{
	tprintf(1.'c'.3.8);
	return 0;
}
Copy the code

It captures a set of mutable parameters, but this is actually to demonstrate the capture of mutable parameters, forcing the use of lambda expressions, otherwise the code might be more concise, we just need to learn how to use this demo, and for the use of variable parameter templates, I won’t expand here.

2.4 The role of capture

When I look at the capture of the lambda has been very strange, at first glance, what is the difference between the capture and transfer parameters, is the value of a variable to lambda expressions for use, but think carefully, it is useful, suppose there is such a case, a company has 999 employees, each employee’s work number from 1 to 999, We now want to find all employees whose id is an integer multiple of 8. A possible code is as follows:

#include <iostream>
#include <array>

int main(a)
{
	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;
}
Copy the code

Expression t capture, and expression t2 use not captured, from the point of code and quantity, they actually difference is not big, but one thing, for the expression of t, the value of x replicated only once, for the t2 expression, every call to generate a temporary variable to hold the value of x, this is actually much more time and space of overhead, however, For this code, this is negligible, but once the data is scaled, it makes a big difference.

As for the role of capture, I only think of this point for the time being, if there is a big man know more about the role, please tell me.

For capture, try not to use [=] or [&] as all capture forms, because out of control, you can’t be sure which variables will be captured, which is prone to unexpected behavior.

Lambda expressions serve as callback functions

A more important use of lambda expressions is that they can be passed in as arguments to functions, through which callback functions can be implemented. For example, in the STL algorithm, some template classes or template functions are often assigned a template argument as a lambda expression. As mentioned in the previous section, I want to count the number of 999 employees whose work number is an integer multiple of 8. An available code is as follows:

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

int main(a)
{
	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;
}
Copy the code

Here, obviously, we specify a lambda expression as a condition. More often, when using collation functions, we specify the collation criteria. We can also use lambda expressions.

4 Lambda expression assignment

Since a lambda expression generates a class object, can it be assigned like a regular class object?

Let’s write some code to try it out:

#include <iostream>
using namespace std;

int main(a)
{
	auto a = [] { cout << "A" << endl; };
	auto b = [] { cout << "B" << endl; };
 
	//a = b; Lambda cannot be assigned
	auto c(a); // make a copy
	return 0;
}
Copy the code

Obviously assignment can’t be done, but copy can. Combined with the compiler’s automatic constructor generation rules, assignment is disabled and copy constructors are not, so you can’t assign a lambda expression to another, but you can initialize a copy.

5 concludes

In short, lambda expressions are used to replace functions that are simpler but more commonly used, according to one definition of lambda expressions. Lambda is used extensively in the STL, and for most STL algorithms, lambda expressions can be used flexibly to achieve the desired effect.

At the same time, lambda is actually a new syntax rule introduced as c++11. It has nothing to do with STL directly, but lambda expressions are used extensively in STL, and it cannot be directly said to be a part of STL.