前言
可能在某时你会发现无论是按值捕获还是按引用捕获均非最佳选择。例如C++11无法将一个move-only对象(std::unique_ptr、std::future)移动进入闭包.又或者当前存在一个copy成本极高但move成本极低的对象(例如STL中的大部分容器),C++11也无法将其移动进入闭包。
这项C++14引入的新功能被称为init capture,它几乎可以完成C++11捕获表单可以执行的所有操作,以及更多功能,唯一的缺点是它无法表现地如同默认捕获,但前一节我们介绍了最好不要使用它们。此外,init捕获的语法略显冗长,因此如果C++11中别的捕获模式已经能够完成任务,我们没有必要去使用init捕获。
init捕获
init捕获可以帮助我们指定
- 由lambda所生成的closure class中data meber的名称
- 该data member的初始化表达式
下述实例将展示如何通过init捕获将std::unique_ptr移动入闭包:1
2
3
4
5
6
7
8
9
10
11
12class Widget { // some useful type
public:
…
bool isValidated() const;
bool isProcessed() const;
bool isArchived() const;
private:
…
};
auto pw = std::make_unique<Widget>(); // create Widget
… // configure *pw
auto func = [pw = std::move(pw)]{ return pw->isValidated()&& pw->isArchived(); };
其中[pw = std::move(pw)]
即为init捕获。“=”左侧是指闭包类中data member的名称,右侧是其初始化表达式。有趣的是,“=”左侧与右侧具备不同的作用域,左侧的作用域为closure class,右侧为lambda定义区。在上述实例中,“=”左侧的名称pw为closure class中的data member,而右侧的名称pw意味着lambda上文中刚刚创建的std::unique_ptr。最终,利用std::move将上文中的std::unique_ptr初始化closure class中的data member。lambda体中的代码处于closure class的作用域内,因此pw代表闭包类数据成员。
在以pw初始化data member之前,我们可能会对堆中的Widget作出修改,这也正对应着注释部分的configur *pw。如果我们当前无需修改,则可省去局部对象pw,直接以make_unique返回值初始化data member:1
auto func = [pw = std::make_unique<Widget>()]{ return pw->isValidated()&& pw->isArchived(); };
init捕获又被称为广义捕获。
在C++11中实现移动捕获
我们明确的是,lambda是一种生成类的方法,那么我们自然可以在C++11中避开lambda直接生成自己需要的class,因此,刚才C++14所写的代码可以被转换为:1
2
3
4
5
6
7
8
9class IsValAndArch {
public:
using DataType = std::unique_ptr<Widget>;
explicit IsValAndArch(DataType&& ptr):pw(std::move(ptr)) {}
bool operator() const{ return pw->isValidated() && pw->isArchived(); }
private:
DataType pw;
};
auto func = IsValAndArch(std::make_unique<Widget>());
虽然多打了很多字,但它至少证明了C++11也能支持移动捕获。
如果你非要既使用C++11的lambda,又需要移动捕获,那你得按照以下步骤行事:
- 将要捕获的对象移动到std::bind生成的函数对象中
- 给lambda一个对“捕获”对象的引用
假设当前我们需要创建一个局部对象std::vector,将一组适当的值放入其中,然后将其移动到闭包中。上述行为在C++14中很容易实现:1
2
3std::vector<double> data; // object to be moved into closure
… // populate data
auto func = [data = std::move(data)]{ /* uses of data */ };// C++14 init capture
在C++11中,同等的实现需要被写为:1
2
3std::vector<double> data; // as above
… // as above
auto func =std::bind([](const std::vector<double>& data){ /* uses of data */ },std::move(data));
类似于lambda表达式,std::bind生成函数对象。std :: bind的第一个参数是一个可调用对象,后续参数表示要传递给该对象的值。bind对象在传递参数给第一个可调用对象时,采用左值拷贝构造,右值移动构造的方式。需要注意的是,()中是一个左值引用,原因在于bind将data传递至闭包后,data是一个左值。此外,为了防止修改数据,我们将一个const-refernce绑定至data,这是因为lambda生成的class默认operator()为const。
由于bind对象内含其所有实参的副本,因此其生命周期与闭包的生命周期一致。总而言之,利用bind模拟移动捕获的要点有三:
- 在C++11中,无法将一个对象移动进入闭包,但可以将其移动进入一个bind对象。
- 在模拟过程中,我们只需要把对象移动进入bind对象,然后以引用形式将其传递给闭包
- 因为bind对象的生命周期与闭包的生命周期相同,所以可以将bind对象中的对象视为处于闭包中