C++11:lambda表达式

目录

一.为什么要有lambda表达式?

二.什么是lambda表达式?

三.如何使用lambda表达式呢?

四.向lambda表达式传递参数

五.使用捕获列表 

值捕获

引用捕获

 隐式捕获

可变lambda 

 捕获this

六、lambda是函数对象


一.为什么要有lambda表达式?

小伙伴们在写代码时有没有遇到这样一种情况呢?当我们对同种类型的对象进行比较时,需要进行不同的比较,如果对于一个商品类,我们可能需要根据商品的价格、生产日期、名字进行不同比较(那么为什么要比较呢?可能是需要按照一定的逻辑进行排序),我们需要创建不同的仿函数,如下所示。

struct Goods
{
	string _name;  // 名字
	double _price; // 价格
	int _date; // 日期
	//...
	Goods(const char* str, double price, int date)
		:_name(str)
		, _price(price)
		, _date(date)
	{}
};
struct ComparePriceLess
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._price < gr._price;
	}
};
struct ComparePriceGreater
{
	bool operator()(const Goods& gl, const Goods& gr)
	{
		return gl._date > gr._date;
	}
};
int main()
{
	vector<Goods> v = { { "苹果", 2.1, 20240511 }, { "香蕉", 3, 20230411 }, { "橙子", 2.2, 20230711 }, { "菠萝", 1.5, 20240621 } };
	sort(v.begin(), v.end(), ComparePriceLess());
	sort(v.begin(), v.end(), ComparePriceGreater());
	return 0;
}

是不是很复杂呢?随着比较参数的增多,上面的写法会越来越复杂,每次为了实现一个algorithm算法,都要重新去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,这些都给编程者带来了极大的不便。因此,针对这个问题,C++11语法中推出了Lambda表达式。

二.什么是lambda表达式?

既然是一个表达式,表达式是不是应该有对应的格式呢?lambda表达式的格式通常如下:

是不是跟普通函数有一点类似之处呢?都有参数列表parameter list,用return type表示返回类型,只不过这里使用的是后置返回类型(trailing return type),都是用花括号括起函数体。

后置返回类型:就是在形参列表后用->指向返回类型。为了表示函数的真正返回类型跟在形参后面,我们用auto作为函数返回类型的占位符

//func接受一个int型的实参,返回一个函数指针,该指针指向一个数组,数组有10个元素,每个元素都是int型
auto func(int i)->int(*)[10];

 既然和函数如此类似?而函数的参数列表可以为空,可以不写返回值(返回类型为void),那么lambda表达式可以这样吗?答案是肯定的,但必须要有捕获列表和函数体,如下所示:

//没有返回类型和参数列表的lambda表达式
[capture list]{function body}

注意:可以将表达式省略,lambda就表示lambda表达式 。

三.如何使用lambda表达式呢?

目前,可以这样理解,当向一个函数传递一个 lambda 时,同时定义了一个新类型和该类型的一个对象:传递的参数就是此编译器生成的类类型的未命名对象。类似的,当使用 auto 定义一个用 lambda 初始化的变量时,定义了一个从lambda生成的类型的对象。

使用lambda表达式,跟使用函数的方法类似,可以直接调用也可以间接调用,调用后的值也可以传递,也可以让lambda表达式去充当一个仿函数等等,当定义一个 lambda时,编译器生成一个与lambda对应的新的(未命名的)类类型。我们拿上面的排序举例,lambda表达式可以作sort的第三个参数comp,我们知道comp是比较函数对象,按照lambda表达式的语法,我们可以这样写:

int main()
{
	vector<Goods> v = { { "苹果", 2.1, 20240511 }, { "香蕉", 3, 20230411 }, { "橙子", 2.2, 20230711 }, { "菠萝", 1.5, 20240621 } };
	sort(v.begin(), v.end(), ComparePriceLess());
	sort(v.begin(), v.end(), ComparePriceGreater());
    //这里用lambda表达式充当函数比较对象,调用sort时,sort会调用lambda表达式
	//sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2)->bool {return g1._price < g2._price; });
    //返回类型可以省略,编译器会自动推理
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price < g2._price; });
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._price > g2._price; });
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._date < g2._date; });
	sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {return g1._date > g2._date; });

	return 0;
}

注意:如果不写返回类型,lambda表达式会根据函数体中的return返回的值推断出返回类型,如果表达式的函数体中没有return,则返回类型为void。

上面是直接使用lambda表达式,同样的,我们也可以间接的使用lambda表达式,对于普通函数,我们一般通过函数指针间接调用函数,对于lambda表达式,我们也要将其赋值给一个auto 对象,通过“调用运算符”调用”lambda表达式,如下所示:

auto f = []{return 6666};
f();//通过调用运算符间接调用
cout << f() << endl;//Output:6666

四.向lambda表达式传递参数

与一个普通函数调用类似,调用一个 lambda时给定的实参被用来初始化 lambda 的形参。通常,实参和形参的类型必须匹配。但与普通函数不同,lambda不能有默认参数。因此,一个 lambda 调用的实参数目永远与形参数目相等。一旦形参初始化完毕,就可以执行函数体了。

五.使用捕获列表 

捕捉列表描述了lambda所在函数域中哪些数据可以被lambda使用,以及使用的方式传值还是传引用。

[var]:表示值传递方式捕捉变量

[=]:表示值传递方式捕获所有父作用域中的变量(包括this)

[&var]:表示引用传递捕捉变量var

[&]:表示引用传递捕捉所有父作用域中的变量(包括this)

[this]:表示值传递方式捕捉当前的this指针

注意:

a. 父作用域是指lambda表达式所在函数的函数体

int main()
{
	int x;//编译器会给它一个默认值
	int y = 0;
	[x, y] {cout << x << y << endl; };//这里定义了一个lambda表达式,但是并没有调用这个表达式
	auto f = [x, y] {cout << x << " " << y << endl; };//这里定义了一个lambda表达式,并用f接受
	f();//这里会调用lambda,此lambda的父作用域就是main函数作用域。
}
//Output:
//-858993460 0

 b. 语法上捕捉列表可由多个捕捉项组成,并以逗号分割。

      [=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量

      [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量

c. 捕捉列表不允许变量重复传递,否则就会导致编译错误。

      比如:[=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复

d. 在块作用域以外的lambda捕捉列表必须为空。(块作用域就是函数体作用域)

e. lambda仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都会导致编译报错。

f. lambda之间不能相互赋值,即使看起来类型相同

g.lambda无法捕获任何static变量、全局变量,但可以在lambda内部直接访问。

默认情况下,从lambda生成的类都包含一个对应该lambda 所捕获的变量的数据成员。类似任何普通类的数据成员,lambda的数据成员也在 lambda 对象创建时被初始化。

int main()
{
	auto f1 = [](int x)->int {cout << x << endl; return 0; };
	f1(1);
	//<lambda_uuid>
	//class <lambda_ff8feb7d01341141aa2501942c589735>
	cout << typeid(f1).name() << endl;
	auto f2 = [](int x)
	{
		cout << x << endl;
		return 0;
	};
	f1(2);
	//class <lambda_c4635ea7e31ca583cc97cf5864d83530>
	cout << typeid(f2).name() << endl;
	return 0;
}
//Output:
//1
//class < lambda_ff8feb7d01341141aa2501942c589735>
//2
//class <lambda_c4635ea7e31ca583cc97cf5864d83530>

值捕获

lambda 采用值捕获的方式,与传值参数类似。

采用值捕获的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在 lambda创建时拷贝,而不是调用时拷贝。

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

引用捕获

一个以引用方式捕获的变量与其他任何类型的引用的行为类似。当我们在 lambda 函数体内使用此变量时,实际上使用的是引用所绑定的对象。

引用捕获与返回引用有着相同的问题和限制。如果我们采用引用方式捕获一个变量,就必须确保被引用的对象在lambda执行的时候是存在的。

注意:

1.lambda 捕获的都是局部变量,如果lambda 在函数结束后执行,因为捕获的引用指向的局部变量已经消失,编译器就会报错,所以当以引用方式捕获一个变量时,必须保证在lambda执行时变量是存在的。

2.函数可以直接返回一个可调用对象,或者返回一个类对象,该类含有可调用对象的数据成员。如果函数返回一个 lambda,则与函数不能返回一个局部变量的引用类似,此 lambda 也不能包含引用捕获。

int main()
{
	int x = 0, y = 1;
	cout << x << " " << y << endl;

	auto f1 = [](int& r1, int& r2)
	{
		int tmp = r1;
		r1 = r2;
		r2 = tmp;
	};
	f1(x, y);
	cout << x << " " << y << endl;//1 0

	//值捕获
	auto f2 = [x,y]()mutable
	{
		int tmp = x;
		x = y;
		y = tmp;
	};
	//捕捉列表中的变量捕获方式可以是值或引用
	//如果是值捕获,想要在lambda表达式的函数体中对值进行修改,就要加上mutable
	//如果是引用捕获,则不用
	//注意:值捕获是在lambda表达式创建时对值进行拷贝,不是调用时拷贝,且对值的修改不影响原值
	f2();
	cout << x << " " << y << endl;//1 0

	//引用捕获
	auto f3 = [&x, &y]()
	{
		int tmp = x;
		x = y;
		y = tmp;
	};
	f3();
	cout << x << " " << y << endl;//0 1
	//对引用捕获的值进行修改会改变原值

	return 0;
}

 隐式捕获

除了显式列出我们希望使用的来自所在函数的变量之外,还可以让编译器根据 lambda体中的代码来推断我们要使用哪些变量。为了指示编译器推断捕获列表,应在捕获列表中写一个s或=。6告诉编译器采用捕获引用方式,=则表示采用值捕获方式。
如果我们希望对一部分变量采用值捕获,对其他变量采用引用捕获,可以混合使用隐式捕获和显式捕获。
当我们混合使用隐式捕获和显式捕获时,注意两点:
a.捕获列表中的第一个元素必须是一个s或=。此符号指定了默认捕获方式为引用或值。
b.显式捕获的变量必须使用与隐式捕获不同的方式。即,如果隐式捕获是引用方式(使用了&),则显式捕获命名变量必须采用值方式,因此不能在其名字前使用&。类似的,如果隐式捕获采用的是值方式(使用了=),则显式捕获命名变量必须采用引用方式,即,在名字前使用&。

 

int main()
{
	int x = 0, y = 1, z = 2;
	auto f1 = [=, &z]() {
		z++;
		cout << x << endl;
		cout << y << endl;
		cout << z << endl;
		};
	
		f1();
	return 0;
}
//Output:
//0
//1
//3

可变lambda 

 捕获this

class AA
{
public:
	void func()
	{
		/*auto f1 = [this]() {
			cout << a1 << endl;
			cout << a2 << endl;
		};*/
		
		//对this进行值捕获
		auto f1 = [=]() {
			cout << this->a1 << endl;//一般省略this
			cout << this->a2 << endl;//一般省略this
			cout << a1 << endl;
			cout << a2 << endl;
		};

		f1();
	}
private:
	int a1 = 11;
	int a2 = 22;
};

int main()
{
	AA().func();
	return 0;
}
//Output:
//11
//22
//11
//22

六、lambda是函数对象

当我们编写了一个 lambda 后,编译器将该表达式翻译成一个未命名类的匿名对象。在 lambda表达式产生的类中含有一个重载的函数调用运算符。例如,对于我们传递给 stable_sort 作为其最后一个实参的 lambda 表达式来说:

//根据单词的长度对其进行排序,对于长度相同的单词按照字母表顺序排序
stable_sort(words.begin(),words.end(),
[](const string sa, const string 6b)
{ 
    return a.size()<b.size();
});

其行为类似于下面这个类的一个未命名对象,

class Shorterstring 
{
public:
bool operator()(const string 6sl, const string 6s2) const
{
     return s1.size()<s2.size(); 
}
};

产生的类只有一个函数调用运算符成员,它负责接受两个 string并比较它们的长度,它的形参列表和函数体与lambda表达式完全一样。默认情况下lambda不能改变它捕获的变量。在默认情况下,由lambda产生的类当中的函数调用运算符是一个const成员函数。如果lambda 被声明为可变的(添加mutable),则调用运算符就不是const的了。

用Shorterstring类的匿名对象替代lambda表达式后,我们可以重写并重新调用stable_sort:

stable_sort(words.begin(),words.end(),Shorterstring());

第三个实参是新构建的 ShorterString 对象,当 stable_sort 内部的代码每次比较两个 string 时就会“调用”这一对象,此时该对象将调用运算符的函数体,判断第一个 string的大小小于第二个时返回true。

当一个 lambda 表达式通过引用捕获变量时,将由程序负责确保 lambda执行时引用所指向的对象确实存在。因此,编译器可以直接使用该引用而无须在lambda产生的类中将其存储为数据成员。相反,通过值捕获的变量被拷贝到lambda中,这种 lambda 产生的类必须为每个值捕获的变量建立对应的数据成员,同时创建构造函数,令其使用捕获的变量的值来初始化数据成员。
lambda 表达式产生的类不含默认构造函数、赋值运算符及默认析构函数;它是否含有默认的拷贝、移动构造函数则通常要根据捕获的数据成员类型而判定。

详细内容请参考《C++ Primer》。

文中若有错误之处,欢迎各位私信或评论区批评指正,感谢!

  • 13
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值