C++ lambda表达式及其原理

概述

C++ 11中引入了新的lamdba表达式,使用也很简单,我最喜欢的是不用给函数取名称,每次给函数取名称都感觉自己读书太少~

1、lambda表达式

lambda表达式可以理解为一个匿名的内联函数。和函数一样,lambda表达式具有一个返回类型、一个参数列表和一个函数体。与函数不一样的是lambda必须使用尾置返回类型。一个lambda表达式表示一个可调用的代码单元。

语法:[capture list] (parameter list) -> return type {function body}

capture list:表示捕获列表,是一个lambda所在函数中定义的局部变量列表
parameter list:表示参数列表
return type:返回类型
function body:函数体

我们可以忽略参数列表和返回类型,但必须永远包含捕获列表和函数体,忽略参数列表等价于指定一个空函数列表,忽略返回类型,lambda会根据函数体中的代码推断出来(如果函数体直接return,则是void类型)。例如:

auto f = [ ] {return 42;};
cout << f() << endl;

lambda的调用方式与普通函数的调用方式相同。

与函数的几点不同在于:

  1. lambda表达式不能有默认参数。因此,一个lambda表达式调用的实参数目永远与形参数目相等
  2. 所有参数必须有参数名
  3. 不支持可变参数

2、捕获列表

如果没有进行捕获,则lambda表达式函数体内只能使用参数列表中的变量。捕获就是明确的指明lambda能使用的局部变量(指调用lambda的地方局部变量)。

这里记住两点就行了:

  1. lambda只有在其捕获列表中捕获一个它所在函数中的局部变量,才能在函数体中使用该变量
  2. 捕获列表只用于非静态局部变量,lambda可以直接使用静态局部变量和在它所在函数之外声明的名字

例如:

void func()
{
	static int i = 10;
	int  j = 20;
	auto f1 = [ ] () { return j; };		//编译出错,没有进行捕获,函数体内不能使用
	auto f2 = [ ] () { return i; };		//静态变量无需捕获
	auto f3 = [j] () { return j; };		//进行了捕获,函数体内可以使用了
};

与函数参数传递类似,变量的捕获方式也可以是值或者引用。这里列出不同的捕获列表的方式,后面进行解释:

C++ lambda表达式及其原理_第1张图片

2.1、值捕获

与传递参数类似,采用值捕获的前提是变量可以拷贝。与参数不同,不捕获的变量的值是在lambda创建时拷贝,而不是调用时拷贝。例如:

void func()
{
	size_t v1 = 42;
	auto f = [ v1 ]  { return v1; };	//使用了值捕获,将v1拷贝到名为f的可调用对象
	v1 = 0;
	auto j = f();	//j为42,f保存了我们创建它是v1的拷贝
}

由于被捕获的值实在lambda创建时拷贝,因此在随后对其修改不会影响到lambda内部对应的值。

默认情况下:如果以传值方式捕获外部变量,则在Lambda表达式函数体中不能修改该外部变量的值

2.2、引用捕获

和函数引用参数一样,一个引用类型的变量在函数体内改变时,实际上使用的是引用所绑定的对象。

void func()
{
	size_t v1 = 42;
	auto f = [ &v1 ]  { return v1; };	//引用捕获,将v1拷贝到名为f的可调用对象
	v1 = 0;
	auto j = f();	//j为42,f保存了我们创建它是v1的拷贝
}

如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存在的。lambda捕获的都是局部变量,这些变量在函数结束后就不复存在了。如果lambda可能在函数结束后执行,这里就会出现问题。

有一些不可拷贝对象,只能使用引用捕获的方式,比如ostream对象。

2.3、隐式捕获

除了显示列出我们希望使用的来自所在函数的局部变量之外,我们还可以让编译器根据函数体中的代码来推断需要捕获哪些变量,这种方式称之为隐式捕获

隐式捕获有两种方式,分别是[=]和[&]。[=]表示以值捕获的方式捕获外部变量,[&]表示以引用捕获的方式捕获外部变量

int main()
{
    int a = 123;
    auto f = [ = ]  { cout << a << endl; };		//值捕获
    f(); 	// 输出:123
    
    auto f1 = [ & ] { cout << a++ << endl; }; 		//引用捕获
    f1();	//输出:123(采用了后++)
    
    cout << a << endl; 		//输出 124
}

2.4、混合方式捕获

lambda还支持混合方式捕获,即同时使用显示捕获和隐式捕获

混合捕获时,捕获列表中的第一个元素必须是 = 或 &,此符号指定了默认捕获的方式是值捕获或引用捕获

需要注意的是:显示捕获的变量必须使用和默认捕获不同的方式捕获。例如:

void func()
{
	int i = 10;
	int  j = 20;
	auto f1 = [ =, &i] () { return j + i; };		//正确,默认值捕获,显示是引用捕获
	auto f2 = [ =, i] () { return i + j; };		//编译出错,默认值捕获,显示值捕获,冲突了
	auto f3 = [ &, &i] () { return i +j; };		//编译出错,默认引用捕获,显示引用捕获,冲突了
};

2.5、修改值捕获的值

在Lambda表达式中,如果以传值方式捕获外部变量,则函数体中不能修改该外部变量,否则会引发编译错误。

如果你希望被值捕获的值被改变,就必须在参数列表首加上关键字mutable

语法变为:[capture list] (parameter list) mutable -> return type {function body}

int main()
{
    int a = 123;
    auto f = [a]()mutable { cout << ++a; }; // 不会报错
    
    cout << a << endl; 	// 输出:123
    f(); 					// 输出:124
}

3、返回类型

在默认的规则下,返回类型如下:

  1. 如果只包含单一的return语句,那么根据return 的类型确定返回类型。
  2. 如果除了return 还有别的语句,那么返回void。

所以,有返回类型时,一定要自己显示的进行说明。

4、lambda表达式的本质

当我们编写了一个lambda之后,编译器将该表达式翻译成一个未命名类的未命名对象该类含有一个重载的函数调用运算符

4.1、采用值捕获

在采用值捕获时,lambda形成的类相当于含有自己的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员。例如两个数加法的方法:

int func()
{	
	int a =10;
	int b = 20;
	auto addfun = [=] (const int c ) -> int { return a+c; };
	
	int c = addfun(b);    
	cout << c << endl;
};

就等同于:

class Myclass
{
public:
	Myclass( int a ) : m_a(a){};	//该形参对应捕获的变量
	
	//该调用运算符的返回类型、形参和函数体都与lambda一致
	int operator()(const int c) const
	{
		return a + c;
	}
	
private:
	int m_a;		//该数据对应通过值捕获的变量
};

lambda表达式产生的类不含有默认构造函数、赋值运算符及默认析构函数。因为不含默认构造函数,因此要想使用这个类必须提供一个实参。

默认情况下,由lambda产生类当中的调用运算符是一个const成员函数,所以值捕获的值不能修改。如果加上mutable相当于去掉const。这样上面的很多限制就能讲通了。

4.2、采用引用捕获

如果lambda采用引用捕获的方式,编译器可以直接使用该引用而无须在lambda对象产生的类中将其存储为数据成员。

唯一需要注意的是,变量将由程序负责确保执行时引用的对象确实存在。

感谢大家,我是假装很努力的YoungYangD(小羊)

参考资料:
《C++ primer 第五版》
https://www.cnblogs.com/lustar/p/7531605.html

你可能感兴趣的:(C\C++)