7.3.7 更多的一些关于lambda的讨论
lambda被纳入C++语言之后,很多人认为lambda让C++11语言看起来更加复杂。一来lambda的语法与之前的C++语法相比起来显得有些独特,二来其基于仿函数的实现,让初学者会感觉一时间找不到它的用途。不过,在考虑过编写仿函数或者使用STL内置的仿函数的复杂性之后,大多数人会肯定lambda的应用价值。
不过要完全用好lambda,必须了解一些lambda的特质。比如lambda和仿函数之间的取舍,如何有效地使用lambda的捕捉列表等。
首先必须了解的是,在现有C++11中,lambda并不是仿函数的完全替代者。这一点很大程度上是由lambda的捕捉列表的限制造成的。在现行C++11标准中,捕捉列表仅能捕捉父作用域的自动变量,而对超出这个范围的变量,是不能被捕捉的。我们可以看看代码清单7-35所示的例子。
代码清单7-35
int d=0;
int TryCapture(){
auto ill_lambda=[d]{};
}
//编译选项:g++ -std=c++11-c 7-3-19.cpp
代码清单7-35所示的例子在一些编译器(如g++)上可以编译通过,但程序员会得到一些编译器的警告,而一些严格遵守C++11语言规则的编译器则会直接报错。而如果我们采用仿函数,则不会有这样的限制,如代码清单7-36所示。
代码清单7-36
int d=0;
class healthyFunctor{
public:
healthyFunctor(int d):data(d){}
void operator()() const{}
private:
int data;
};
int TryCapture(){
healthyFunctor hf(d);
}
//编译选项:g++ -std=c++11-c 7-3-20.cpp
在代码清单7-36中的healthyFunctor说明了两者的不同。更一般地讲,仿函数可以被定义以后在不同的作用域范围内取得初始值。这使得仿函数天生具有了跨作用域共享的特征。
总地来说,lambda函数被设计的目的,就是要就地书写,就地使用。使用lambda的用户,更倾向于在一个屏幕里看到所有的代码,而不是依靠代码浏览工具在文件间找到函数的实现。而在封装的思维层面上,lambda只是一种局部的封装,以及局部的共享。而需要全局共享的代码逻辑,我们则还是需要用函数(无状态)或者仿函数(有状态)封装起来。
简单地总结一下,使用lambda代替仿函数的应该满足如下一些条件:
❑是局限于一个局部作用域中使用的代码逻辑。
❑这些代码逻辑需要被作为参数传递。
此外,关于捕捉列表的使用也存在有很多的讨论。由于[=],[&]这些写法实在是太过方便了,有的时候,我们不会仔细思考其带来的影响就开始滥用,这也会造成一些意想不到的问题。
首先,我们来看一下[=],除去我们之前提过的,所有捕捉的变量在lambda声明一开始就被拷贝,且拷贝的值不可被更改,这两点需要程序员注意之外,还有一点就是拷贝本身。这点跟函数参数按值方式传递是一样的,如果不想带来过大的传递开销的话,可以采用引用传递的方式传递参数。
其次,我们再来看一下[&]。如我们之前提到过的,通过引用方式传递的对象也会输出到父作用域中。同样的,父作用域对这些对象的操作也会传递到lambda函数中。因此,如果我们代码存在异步操作,或者其他可能改变对象的任何操作,我们必须确定其在父作用域及lambda函数间的关系,否则也会产生一些错误。
通常情况下,在使用[=]、[&]这些默认捕捉列表的时候,我们需要考察其性能、与父作用域如何关联等。捕捉列表是lambda最神奇也是最容易犯错的地方,程序员不能一味图方便了事。