飞道的博客

Lambda表达式从用到底层原理

286人阅读  评论(0)


前言

lambda式作为一种创建函数对象的手段,实在太过方便,对c++日常软件开发产生极大影响,所以特来学习。


一、lambda函数基本使用

lambda函数是C++11标准新增的语法糖,也称为lambda表达式或匿名函数。
lambda函数的特点是:距离近、简洁、高效和功能强大。
示例:

[](const int& no) -> void {
    cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n"; };

语法:

参数列表

参数列表是可选的,类似普通函数的参数列表,如果没有参数列表,()可以省略不写。
与普通函数的不同:

  • lambda函数不能有默认参数。
  • 所有参数必须有参数名。
  • 不支持可变参数。

返回类型

用后置的方法书写返回类型,类似于普通函数的返回类型,如果不写返回类型,编译器会根据函数体中的代码推断出来。
如果有返回类型,建议显式的指定,自动推断可能与预期不一致。

auto f=[](const int& no) ->double{
   
	cout << "亲爱的" << no << "号:我是一只傻傻鸟。\n";
};

此时auto判定f为double类型;

函数体

类似于普通函数的函数体。

捕获列表

通过捕获列表,lambda函数可以访问父作用域中的非静态局部变量(静态局部变量可以直接访问,不能访问全局变量)。
捕获列表书写在[]中,与函数参数的传递类似,捕获方式可以是值和引用。
以下列出了不同的捕获列表的方式。

值捕获

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

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

由于被捕获的值是在lambda函数创建时拷贝,因此在随后对其修改不会影响到lambda内部的值
默认情况下,如果以传值方式捕获变量,则在lambda函数中不能修改变量的值。

引用捕获

和函数引用参数一样,引用变量的值在lambda函数体中改变时,将影响被引用的对象。

size_t v1 = 42;
auto f = [ &v1 ]  {
    return v1; };	 // 引用捕获,将v1拷贝到名为f的可调用对象。
v1 = 0;
auto j = f();	   // j为0。

如果采用引用方式捕获变量,就必须保证被引用的对象在lambda执行的时候是存在的

隐式捕获

除了显式列出我们希望使用的父作域的变量之外,还可以让编译器根据函数体中的代码来推断需要捕获哪些变量,这种方式称之为隐式捕获。
隐式捕获有两种方式,分别是[=]和[&]。[=]表示以值捕获的方式捕获外部变量,[&]表示以引用捕获的方式捕获外部变量。

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

lambda函数还支持混合方式捕获,即同时使用显式捕获和隐式捕获。
混合捕获时,捕获列表中的第一个元素必须是 = 或 &,此符号指定了默认捕获的方式是值捕获或引用捕获。
需要注意的是:显式捕获的变量必须使用和默认捕获不同的方式捕获。例如:

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

在lambda函数中,如果以传值方式捕获变量,则函数体中不能修改该变量,否则会引发编译错误。
在lambda函数中,如果希望修改值捕获变量的值,可以加mutable选项,但是,在lambda函数的外部,变量的值不会被修改。

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

lambda可以抛出异常,用throw(…)指示异常的类型,用noexcept指示不抛出任何异常。

二、lambda表达式使用的注意事项

避免默认捕获模式

按引用的默认捕获模式可能导致空悬引用,一旦由lambda式所创建的闭包越过了局部变量或形参的生命周期,那么闭包内的引用就会空悬(即必须保证被引用的对象在lambda执行的时候是存在的
(有没有空悬引用其实就是看的生命周期,那个长)

既然引用有导致空悬引用的风险,那是不是可以用按值捕获呢。按值的默认捕获也有可能存在空悬的风险。如按值捕获了一个指针以后,在lambda式创建的闭包中持有的是这个指针的副本,但并无办法阻止lambda式之外的代码去针对该指针实施delete操作所导致的指针副本空悬。

对于类的方法中使用lambda,如果使用到了类的成员变量,则会出现无法被捕获的错误。如下:

void Widget::addFilter() const
{
   
	filters.emplace_back(
		[divisor](int value)		//错误
		{
    return value % divisor==0;}	//局部没有可捕获的divisor(divisor既不是局部变量,也不是形参)
	);
}

解决这一问题,关键在于一个裸指针隐式应用,这就是this。每一个非静态成员函数都持有一个this指针,然后每当提及该类的成员变量时都会用到这个指针。
所以此上的代码的lambda函数被捕获的实际上是Widget的this指针,而不是divisor。
代码如下 :

void Widget::addFilter() const
{
   
	auto currentObjectPtr=this;
	filters.emplace_back(
		[currentObjectPtr](int value)
		{
   return value%currentObjectPtr->divisor==0;}
	);
}

这就相当于lambda闭包的存活与它含有其this指针副本的Widget对象的生命期是绑在一起的

对于以static声明的静态变量,可以在lambda内使用,但是它们不能被捕获

三、lambda表达式底层实现原理

class Add{
   
public:
	Add(int n):_a(n){
   
	}

	int operator()(int n){
   
		return _a + n;
	}
private:
	int _a;
};

int main(){
   
	int n = 2;
	Add a(n);
	a(4);

	auto a2 = [=](int m)->int{
   return n + m; };
	a2(4);
	return 0;
} 


 

从上面的代码中可以看到,仿函数与lambda表达式完全一样

实际当我们编写了一个lambda表达式之后,编译器将该表达式翻译成一个未命名类的未命名对象。该类含有一个operator()。
整个lamda表达式,编译的时候,

  1. 编译器给你自动生成一个形如 <lambda_b328511335c7e943aa98460a349659c7> 的类
  2. 然后把捕获列表中的参数,都按照你的要求(值捕获, 引用捕获)包装到这个类的成员里面
  3. 编译器生成一个 operator() 重载函数, 最后你对lamda的调用就是对函数对象的调用了, 捕获的参数早给你准备好了

采用值捕获

采用值捕获时,lambda函数生成的类用捕获变量的值初始化自己的成员变量。
例如:

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

等同于:

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

默认情况下,由lambda函数生成的类是const成员函数,所以变量的值不能修改。如果加上mutable,相当于去掉const。这样上面的限制就能讲通了。

采用引用捕获

如果lambda函数采用引用捕获的方式,编译器直接引用就行了。
唯一需要注意的是,lambda函数执行时,程序必须保证引用的对象有效。


转载:https://blog.csdn.net/weixin_52259848/article/details/128401710
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场