C++2.0新特性——lambda表达式

lambda表达式

一、概念

1、lambda表达式:表达式的一种,是源代码的组成部分。常用于创建闭包并将其用作传递给函数的实参,例如:

vector<int> vec={1,2,3};
find_if(vec.begin(),vec.end(),[](int val){return 0<val && val<10;});

第三个参数即为lambda表达式。
2、闭包:闭包是lambda表达式创建的运行期对象,根据不同的捕获方式(按引用或值),闭包会持有数据的副本或引用。上式中,闭包就是作为第三个参数在运行期传递给find_if的对象。
3、闭包类:实例化闭包的类。每个lambda表达式都会触发编译期生成一个独一无二的闭包类,而闭包中的语句会变成它的闭包类成员函数的可执行指令。

lambda表达式存在于编译期,闭包存在于运行期

二、默认捕获方式

按引用捕获:例如

[&](int value){return value%divisor==0;}

按引用捕获会导致闭包包含指向局部变量的引用,或者指向定义lambda表达式作用域内形参的引用。一旦闭包越过了其生命期,闭包内的引用就会空悬。例如:

using FilterContainer = vector<function<bool(int)>>;
FilterContainer filters;//筛选函数的容器

void addDivisorFilter()
{
	auto cv1 = computeValue1();//计算某个值
	auto cv2 = computeValue2();//计算某个值

	auto divisor = computeDivisor(cv1, cv2);//局部变量

	filters.emplace_back([&](int value) {return value % divisor == 0; })//出错,对divisor的指向可能空悬
}

上述代码中,lambda表达式指向局部变量divisor的引用,但是在addDivisorFilter函数执行完成之后就被系统回收了。使用此筛选器,从filters被创建的那一刻起,就会产生未定义的行为。修改:

filters.emplace_back([&divisor](int value) {return value % divisor == 0; })

通过显示捕获divisor,此时lambda表达式的生存期依赖于divisor的生命期。长远来看:显式地列出lambda表达式的局部变量或形参是更好的方式。

但是,有些情况是无法确定lambda表达式依赖的变量的,此时无法显式捕获,此时解决空悬的办法是按值传递
假设此时对类Widget实施筛选器,代码如下:

class Widget {
public:
	void addFilter() const;//向filters中添加条目
private:
	int divisor;
};

void Widget::addFilter() const
{
	filters.emplace_back([=](int value) {return value % divisor == 0; })//错误
}

捕获只能针对在创建lambda表达式的作用域内可见的非静态局部变量(包括形参)。而divisor是Widget类的成员变量,并非局部变量。此时无论是默认捕获[],还是显式捕获[divisor]都无法编译通过。

原因在于,类的非静态成员都含有的this指针,因此默认的捕获方式其实捕获的是this指针,因此lambda表达式的生命期和Widget绑定在一起的,当使用智能指针时,会出现空悬的情况:

void doSomeWork()
{
	auto pw = make_unique<Widget>();
	pw->addFilter();
}//Widget被回收,filters此时有空悬指针

解决办法:将想捕获的成员变量divisor复制到局部变量中。

void Widget::addFilter() const
{
	auto divisorTemp=divisor;//复制成员变量
	filters.emplace_back([divisorTemp](int value) //捕获副本
	{return value % divisorTemp== 0; })//使用副本
}

lambda表达式可能不仅依赖局部变量或形参,还可能依赖静态存储器对象(static)。例如:

void addDivisorFilter()
{
	static auto cv1 = computeValue1();
	static auto cv2 = computeValue2();

	static auto divisor = computeDivisor(cv1, cv2);

	filters.emplace_back([=](int value)//未捕获任何东西(只能捕获非静态局部变量或形参)
	 {return value % divisor == 0; });//指向了静态变量divisor

	++divisor;//意外修改了divisor,导致filters中每个行为都不一样
}

总结:尽量避免使用默认捕获模式。按引用的默认捕获会导致指针空悬;按引用的默认捕获会受到this的影响,以及静态生命期对象的影响。

三、初始化捕获

初始化捕获可将一个只移对象(右值)移入闭包。使用初始化捕获,可以指定由lambda表达式生成的闭包类中的成员变量的名字;同时指定一个表达式,用以初始化该成员变量。C++14支持初始化捕获,实例代码如下:

class Widget {
public:
	bool isValidated() const;
	bool isProcessed() const;
	bool isArchived() const;
private:
};

auto pw = make_unique<Widget>();//创建Widget
...                   //配置*pw,达到被捕获的合适状态
auto func = [pw = move(pw)]{ return pw->isValidated() && pw->isArchived(); }//使用move初始化闭包类的数据成员

其中,最后一条语句就是初始化捕获。“=”左侧的就是闭包类成员变量的名字,右侧是其初始化表达式。左侧的pw指向的是闭包类的成员变量,作用域是闭包类的作用域;右侧的pw指向的是创建Widget那一条语句的对象,作用域是与lambda表达式定义处的作用域相同。因此,表达式“pw = move(pw)”表示:在闭包中创建一个成员变量pw,然后针对局部变量pw实施move的结果来初始化该成员变量。

若无需配置*pw,则该语句可直接简化为:

auto func = [pw = make_unique<Widget>()]{ return pw->isValidated() && pw->isArchived(); 

在C++14中,初始化捕获又称广义捕获。可以捕获表达式。

在C++11中若要想完成初始化捕获,可以借助bind()函数来模拟,步骤:
1、把需要捕获的对象移动到bind产生的函数对象中;
2、给lambda表达式一个指向欲捕获的对象的引用。

例如:创建一个局部的vector对象,将其放入合适的一组值,然后移入闭包。C++14实现如下:

vector<double> data;//欲移入闭包的对象
auto func=[data=move(data)]{...}//C++14初始化捕获

C++11实现如下:

vector<double> data;
auto func=bind([](const vector<double>& data){...},move(data));//C++11模拟初始化捕获

bind与lambda类似,也生成函数对象,称bind返回的对象为**绑定对象。**第一个实参是个可调用对象,后面的实参表示传给该对象的值。

绑定对象func含有传递给bind所以实参的副本。对于左值实参,绑定对象内对应的对象内实施的是复制构造,对于右值实参,实施的是移动构造。当func被调用时,func内经移动构造得到的data副本会作为实参传递给前面的lambda表达式。

类似的:

auto func=[pw=make_unique<Widget>()]{....};//C++14
auto func=bind([](const unique_ptr<Widget>& pw){...},make_unique<Widget>());          //C++11     

四、lambda表达式中的完美转发(C++14)

例如:

auto f=[](auto x){return func(normalize(x)));

上式中lambda表达式对x实施的唯一动作是转发给normalize函数,若normalize区别对待左右值时,lambda总会传左值,即使传给lambda表达式的实参是个右值。很容易想到的改进方式是使用完美转发:

auto f=[](auto&& x){return func(normalize(forward<?>(x)))};

设计过程中会发现forward的形参类型是无法确定的,通过探查x的类型可以知道传入的实参是左值还是右值,作为forward的形参,decltype(x)。当传入的是个左值,则会产生左值引用;当传入右值,则会产生右值引用。

auto f=[](auto&& x){return func(normalize(forward<decltype(x)>(x)))};

注:forward惯例是:用类型参数为左值引用表明想要返回左值,用非引用类型来表明返回右值。由于引用折叠的规则,使得右值引用和非引用类型在发生完美转发时产生的结果是一样的。

T&& forward(remove_reference_t<T>& param)
{
	return static_cast<T&&>(param);
}

若要完美转发Widget类型的右值,即应该采用非引用类型实例化:

Widget&& forward(Widget& param)
{
	return static_cast<Widget&&>(param);
}

若将T指定为右值引用,则:

Widget&& && forward(Widget& param)
{
	return static_cast<Widget&& &&>(param);
}

引用折叠之后:

Widget&& forward(Widget& param)
{
	return static_cast<Widget&&>(param);
}

效果是一样的。


版权声明:本文为qq_33766994原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
THE END
< <上一篇
下一篇>>