使用初始化捕获将对象移入闭包
如果你想要把一个只能被移动的对象(例如std::unique_ptr
或std::future
类型的对象)放入闭包,C++11是无法实现的。如果你要拷贝的对象拷贝开销非常高,但移动的成本却不高(例如标准库中的大多数容器),并且你希望的的是移动该对象,而非拷贝它。但是,C++11中也还是没有让你实现这一点的途径。
如果你的编译器支持C++14,那又是另一回事了。它能支持将对象移动到闭包中。
缺少移动捕获被认为是C++11的一个缺点。最直接的补救措施本是在C++14中添加这一特性,但标准委员会却提出了一种全新的捕获机制。按移动的捕获只不过属于该机制能够实现的多种效果之一罢了。这种能力叫做初始化捕获。它几乎可以完成C++11捕获形式的所有工作,甚至能完成更多功能。默认的捕获模式使得你无法使用初始化捕获表示、
使用初始化捕获可以让你指定:
- 从
lambda
生成的闭包类中的数据成员名称。 - 初始化该成员变量的表达式。
以下是如何使用初始化捕获将std::unique_ptr
移动到闭包内
class Widget{
public:
...
bool isValidated() const;
bool isProcessed() const;
bool isArchived() const;
private:
...
};
auto pw = std::make_unique<Widget>(); //创建Widget
... //配置*pw
auto func = [pw = std::move(pw)]{ //采用std::move(pw)
return pw->isValidated() && pw->isArchived(); //初始化闭包类的数据成员
};
[pw=std::move(pw)]
这段代码就是初始化捕获,位于”=“左侧的,就是你所指定的闭包类成员变量的名字,而位于其右侧的则是其初始化表达式。可圈可点之处在于,”=“的左右两侧位于不同的作用域。左侧作用域就是闭包类的作用域,而右侧的作用域则与lambda
式加以定义之处的作用域相同。在上述例子中,”=“左侧的名字pw
指向的式闭包类的成员变量,而右侧的名字指向的是则是在lambda
式上面一行声明的对象,即经由调用make_unique
所初始化的对象。所以,pw=std::move(pw)
表达了在闭包中创建一个成员变量pw
,然后针对局部变量pw
实施std::move
的结果来初始化该成员变量。
该例中”配置*pw
“这条注释表明,在Widget
经由std::make_unique创建之后,并在指向到该Widget的std::unique_ptr被lambda捕获之前,该Widget会在某些方面加以修改。如果这样的配置并非必要动作,即,经由std::make_unique创建的Widget对象已具备被lambda式捕获的合适状态,则作为局部变量pw亦非必要,因为闭包类成员变量可以由std::make_unique实施初始化。
auto func = [pw = std::make_unique<Widget>()]
{ return pw->isValidated() && pw->isArchived(); };
C++11的”捕获“概念在C++14中得到了显著的泛化,因为在C++11中不可能捕获一个表达式的结果。因此,初始化捕获还有另一个美名,叫做广义lambda
捕获。
如果你使用的编译器缺少对C++14初始化捕获的支持,该怎么办?在不支持按移动捕获的语言中,又该如何实现按移动捕获?
一个lambda
表达式不过是生成一个类并且创建一个该类的对象的手法罢了。并不存在lambda
能做,而你手工做不到的事情。以上面所见的C++14示例代码为例,就可以如下用C++11写。
class 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>());
这比起撰写一个lambda
式长了不少,但是它并未改变一个事实,即在C++11中,如果你想要一个支持对成员变量实施移动初始化的类,那么需要多花些时间敲键盘。
在C++11中可以采用以下方法模拟,只需要
- 把需要捕获的对象移动到
std::bind
产生的函数对象中 - 给到
lambda
式一个指向到欲捕获的对象的引用
假如你想要创建一个局部的std::vector
对象,向其放入适合的一组值,然后将其移入闭包。在C++14中,这是举手之劳:
std::vector<double> data; //欲移入闭包的对象
... //灌入数据
auto func = [data = std::move(data)] { /*对数据加以运用*/ };
代码的关键部分已加以突显:欲移动的对象的类型(std::vector<double>
)和该对象的名字(data),还为初始化捕获而准备的初始化表达式(std::move(data)
)。
使用C++11撰写的等价代码如下,我也把同样的关键部分加以凸显:
std::vector<double> data;
...
auto func = std::bind(
[](const std::vector<double>& data)
{ /*对数据加以运用*/ },
std::move(data)
);
和lambda
表达式类似,std::bind
也生成函数对象。std::bind
返回的函数对象为绑定对象。std::bind
的第一个实参是个可调用对象,接下来的所有实参表示传给该对象的值。
绑定对象含有传递给std::bind
所有实参的副本。对于每个左值实参,在绑定对象内的对应的对象内对其实施的是拷贝构造;而对于每个右值实参,实施的则是移动构造。在这个例子中,第二个实参是个右值(即std::move
的结果),所以data
在绑定对象中实施的是移动构造。而该移动构造动作正是实现模拟移动捕获的核心所在,因为把右值移入绑定对象,正是绕过C++11无法将右值移入闭包的手法。
当一个绑定对象被”调用“(即,其函数调用运算符被唤起)时,它所存储的实参会传递给原先传递给std::bind
的那个可调用对象。在本例中,也就是当func
(绑定对象)被调用时,func
内经由移动构造出所得到的data
的副本就会作为实参传递给那个原先传递给std::bind
的lambda
式。
这个lambda
和C++14版本的lambda
长得一样,但是多加了一个形参data
,它对应于我们的伪移动捕获对象。该形参是个指向绑定对象内的data
副本的左值引用(而不是右值引用,因为虽然初始化data
副本的表达式是std::move(data)
,但data
的副本本身是一个左值)。这么一来,在lambda
内对data
做的操作,都会实施在绑定对象内移动构造而得的data
的副本之上。
默认情况下,lambda
生成的闭包类中的operator()
成员函数会带有const
饰词,结果,闭包里的所有成员变量在lambda
式的函数体内都会带有const
饰词。但是,绑定对象里移动构造得到的data
副本却并不带有const
饰词,所以,为了防止该data
的副本在lambda
式内被意外修改,lambda
的形参就声明为常量引用。但如果lambda
式声明带有mutable
饰词,闭包里的operator()
函数就不会在声明时带有const
饰词。
auto func =
std::bind(
[](std::vector<double>& data) mutable
{ /*对数据加以运用*/ },
std::move(data)
);
因为绑定对象存储着传递给std::bind
所有实参的副本,在本例中的绑定对象就包含一份由作为std::bind
的第一个实参的lambda
式产生的闭包的副本。这么一来,该闭包的生命期和绑定对象就是相同的。这一定很重要,因为这意味着只要闭包还存在,则绑定对象内的伪移动捕获对象也存在。
- 移动构造一个对象入C++11闭包是不可能实现的,但移动构造一个对象入绑定对象则可能实现
- 欲在C++11中模拟移动捕获包括以下步骤:先移动构造一个对象入绑定对象,然后按引用把该移动构造所得的对象传递给
lambda
式 - 因为绑定对象的生命期和闭包相同,所以针对绑定对象中的对象和闭包里的对象可以采用同样的手法加以处理。
关于使用std::bind
模拟移动捕获。下面是C++14在闭包内创建std::unique_ptr
的代码
auto func = [pw = std::make_unique<Widget>()]
{ return pw->isValidated() && pw->isArchived(); }
以下是使用C++11创建的模拟代码
auto func = std::bind([](const std::unique_ptr<Widget>& pw)
{return pw->isValidated() && pw->isArchived();},
std::make_unique<Widget>()
);
要点速记
- 使用C++14的初始化捕获将对象移入闭包
- 在C++11中,经由手工实现的类或
std::bind
去模拟初始化捕获