再说C++的lambda表达式

  一直以来,lambda仿佛都是像是脚本语言的专利,对于C/C++这类系统级强类型的编译语言来说,实现匿名函数几乎是不可想象的,不过现在C++11已经支持lambda创建匿名函数了。对于一些小而简单的代码,创建匿名函数再方便不过了,因为围绕程序员最头疼的难题就是吃饭吃什么,变量、函数该取什么名,所以Lambda支持必然深受大家的喜爱。
  传统上,大家都是系统先定义一个含有operator()的命名类,然后在创建该类的一个对象,最终在合适的位置通过该对象调用函数。其实,这个步骤就算是lambda的前身了,lambda语法自动创建匿名类和匿名对象,像是上面传统可调用类实现繁琐步骤的快速实现,一处定义且只使用一次,可以方便的结合各种标准算法库作为谓词使用,或者智能指针的deleter定义,同时也可以创建闭包任务执行各种回调等任务。总之结合C++可调用对象的概念,可以大大简化了项目的设计和实现风格。

1
std::find_if(container.begin(), container.end(), [](int val) { return val > 0 && val < 10; });

  本来以为lambda把捕获搞清楚就可以了,但是细究下去还是需要梳理一下。
  lambda的组件包括:一个可能为空的捕获列表、一个可选的参数列表、一个可选的mutable修饰符、一个可选的noexcept修饰符、一个可选的->返回类型、一个执行表达式体。

1
[ capture ] ( params ) opt -> ret { body; }

一、捕获

1.1 lambda的捕获类型

  有些时候,我们需要控制lambda是否允许和如何访问局部名字,这时候就需要指明捕获信息。在lambda中,我们可以选择的捕获类型有下面这几种:
  []: 空捕获列表,意味着lambda无法使用期外层上下文中的任何局部名字,其内部执行体所需的符号只能从实参或者非局部变量中获取;
  [&]: 引用隐式捕获,其所有的局部名字都能使用,所有的局部变量都用引用访问;
  [=]: 按值隐式捕获,所有的局部名字都能使用,所有名字都是指向局部变量的副本,这些副本是在lambda表达式的调用点获得的;
  [捕获列表]: 只捕获列表中的变量,不捕获其他变量,捕获列表中可以出现this。
  [&, 捕获列表]、[=, 捕获列表]: 某些变量进行特殊的捕获方式,其他变量采用默认捕获方式,捕获列表中可以出现this。
  在使用中,当我们考虑选择捕获类型的时候,使用捕获列表可以具有更细粒度的捕获控制,而如果希望局部对象可以写入修改,或者捕获的对象很大会有拷贝负担,则可以考虑使用引用捕获;但是lambda用于闭包执行的话,其有效期可能会超过其调用者,而且如果lambda的创建和执行在不同线程的话,一般通过按值捕获会更加安全。
  可变参数的捕获方式如下:

1
2
3
4
template<typename... Var>
void f(int s, Var... params){
auto helper = [&s, &v...]{ return s*(h(v...) + h2(v...)); }
}

  在使用lambda的时候,捕获机制不可滥用,很多时候我们也可以考虑采用传递实参的方式来访问名字,这通常更加的直白而不容易发生错误。

1.2 lambda的其他细节

  因为全局变量和名字空间变量永远是可访问的,所以只要确保他们在可访问的作用域中,lambda不需要捕获就可以访问他们。
  当lambda被作用在类的成员函数中的时候,通常做法是把this添加到捕获列表中,这样类的成员就位于可被捕获的名字集合中了,看上去lambda具有和当前类的成员函数同样的访问权限了,这种做法在类成员函数的实现中尤其奏效,因为实现函数通常都需要访问类的成员变量。注意的是这种方式实际”效果”是通过引用方式捕获的,所以可以保存成员变量的修改。
  默认情况下大家都不希望修改函数对象的状态,因此默认情况下lambda是无法修改类的成员,虽然他们是按值捕获对象,等于是函数对象的成员变量,修改他们也不会影响到外部被捕获的变量,但是就是不让你修改他,也就是类似于operator()实质上是一个const成员函数。要改变这个特性,则在lambda定义的时候位于参数列表之后添加mutable关键字即可。

1.3 lambda的类型和返回类型

  a. lambda的返回类型
  如果一个lambda表达式不接受任何参数的话,其参数列表可以被忽略,因此最简洁的lambda的形式为:[]{}。
  如果lambda的主体不包含return语句,则该lambda的返回类型就是void;如果lambda的主题只包含一条return语句,则该lambda的返回类型就推导成是return表达式的类型;如果有多条return的复杂情况,则必须显式地尾置指明一个返回类型。
  不过,如果return结果是新定义的初始化列表,则lambda无法推导出其返回类型;而将其定义为一个变量返回则是可以的。

1
2
auto f = [](int i) { return {i, i+ 1} }; // Error
auto f = [](int i) { auto id = {i, i+1}; return id; };

  b. lambda的类型
  因为lambda是个匿名的局部类型,所以任意两个lambda的类型都不相同。lambda除了直接作为参数之外,还可以用于对auto或者std::function的变量进行初始化或者保存。
  当lambda什么也不捕获的时候,我们还可以将它赋值给一个函数指针,而一旦捕获列表不为空[],则其不再能转换成一个函数指针了。

二、lambda使用方式讨论

2.1 避免使用默认捕获模式

  C++的lambda有两种默认的捕获类型:[=]和[&],在定义lambda的时候最好不要使用这种默认捕获类型的形式。
  [&]捕获让lambda持有对定义位置的局部变量或者参数的引用,但是一旦lambda的生命周期被延长以至在执行的时候局部变量和参数已经不存在,那么就会访问之就会是个悬垂的引用,因为lambda可以被存储在容器中,也可以被return或者拷贝出去,这种情况有意或者无意情况下是很容易发生的,除非确定lambda创建后就会在本地立即执行,此时引用捕获才是绝对安全的。此时即使使用&显式捕获,虽然不能杜绝上面的问题,但是可以明确表达出该lambda所依赖的局部变量,出现问题也比较容易排查。
  [=]也不是什么省油的灯。首先,如果捕获的是指针类型,如果是智能指针还好,如果是原始指针的话,还是会可能出现指针悬垂的问题。
  还有,lambda可以捕获的对象,是当前作用域中的non-static局部变量和参数列表,对于全局变量、命名空间变量、文件或者函数的static静态变量是不用捕获就可以直接访问的(如果作用域可以),所以如果需要访问类成员,则必须捕获this指针才可以([]、[member]会出错,[=]或者[this]则可以),因为this指针算是个裸指针,如上面所述的情况,还是可能会出现悬垂的问题,安全的使用方法是将成员变量的值拷贝一份成局部变量,然后按值捕获这个局部变量即可。

1
2
3
4
5
6
void Widget::addFilter() const {
auto divisorCopy = this->divisor;
filters.emplace_back([=] (int value) {
return value % divisorCopy == 0;
});
}

2.2 捕获移动对象

  对于lambda捕获,比较大的对象如果采用移动的方式则可以获得性能的改善,而且std::unique_ptr、std::future等类型是只支持移动不支持拷贝的,所以lambda对移动捕获还是有需求的。可惜的是直接的移动支持只有在C++14标准才被支持(被称为init capture),C++11的话需要使用其他小技巧来实现上述移动支持的功能。
  其基本思路就是在可调用对象lambda的外层再借助std::bind()工具包装一下生成一个函数对象(function object),通过std::bind()创建的函数对象会包含所有传递给std::bind()的额外参数,而且这些参数如果是左值的话就会进行拷贝构造,如果是右值的话就会进行移动构造,而当执行调用的时候,参数就会自动传递给内部的可调用对象(lambda),内部可调用对象可以安全通过引用的方式访问外围移动构造的参数。因为function object的生命周期几乎和lambda的生命周期一致,所以引用访问本身也是安全的。
  就是通过上面迂回的方式,以实现效果上的lambda移动捕获参数的效果。因为lambda默认是const的,但是移动构造的参数是non-const的,如果需要修改的话就可以设置以non-const引用的方式访问之。

1
2
3
4
5
6
7
auto func1 = std::bind([](const std::unique_ptr<Widget>& pw) {
return pw->isValidated();
}, std::make_unique<Widget>());
auto func2 = std::bind([](std::unique_ptr<Widget>& pw) mutable {
return pw->isValidated();
}, std::make_unique<Widget>());

  哎,可惜因为生产系统过于老旧,这些东西憋着没法用,装逼说来就是像绑着脚链在跳舞,各种不能用就是感觉不爽,也真是可惜了。

本文完!

参考