条款32.使用初始化捕获将对象移入闭包

使用初始化捕获将对象移入闭包

如果你想要把一个只能被移动的对象(例如std::unique_ptrstd::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::bindlambda式。

这个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去模拟初始化捕获
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值