Item 32: Use init capture to move objects into closures

有些时候,按值捕获和按引用捕获都不是我们想要的。比如想把一个只能移动的对象(std::unique_ptr 或 a std::future)放入闭包,在C++ 11中就没有直接提供相应的支持。同样地,如果想把一个既能拷贝也可以移动的对象移动进闭包,在C++ 11中也没相应的支持。

上面的问题在C++ 14中被彻底的解决了。但是,在C++ 11中有近似达成移动捕获的方法。其实,标准委员会也意识到了这个问题,所以在C++ 14中马上就对移动对象进入闭包做了支持。但是,他们采用一种更灵活的全新捕获机制。这种新能力被称为初始化捕获(init capture)。它可以做到C++ 11中捕获形式能做到的所有事情,而且还能做的更多。初始化捕获做不了默认捕获能做的事情,但是Item 31中建议我们尽量避免使用默认捕获模式。

使用初始化捕获模式,有能力做两件事情:

  1. 指定闭包类的成员名称;
  2. 指定一个表达式来初始化这个成员;

举个例子:移动一个std::unique_ptr对象到 lambda 闭包:

class Widget {                                  // some useful type
public:bool isValidated() const;
    bool isProcessed() const;
    bool isArchived() const;
private:};

auto pw = std::make_unique<Widget>();           // create Widget; see
                                                // Item 21 for info on
                                                // std::make_unique// configure *pw

auto func = [pw = std::move(pw)]                // init data mbr
            { return pw->isValidated()          // in closure w/
                     && pw->isArchived(); };    // std::move(pw)

在捕获列表中,“=” 左边的 pw 是指定的 lambda 闭包的成员名称。“=” 右边的 std::move(pw) 是指定的用于初始化闭包成员 pw 的表达式。“=” 两边的作用域也不一样,左边的作用域在lambda闭包中,右边的作用域是lambda表达式被定义的作用域。
如果上面代码中的“configure *pw”部分不是实际所需要的业务逻辑,则代码可以进一步改为:

auto func = [pw = std::make_unique<Widget>()]   // init data mbr
            { return pw->isValidated()          // in closure w/
                    && pw->isArchived(); };     // result of call
                                                // to make_unique

因为在C++ 11中不能捕获表达式的结果,所以在C++ 14中,初始化捕获又称为广义lambda捕获(generalized lambda capture)。

lambda表达式仅仅一种生成类并创建该类对象的手法罢了。所以,并不存在lambda能做,而我们手动做不了的事情。以上面C++ 14中例子为例,在C++ 11中可写作:

class IsValAndArch {                            // "is validated
public:                                         // and archived"
    using DataType = std::unique_ptr<Widget>;
    
    explicit IsValAndArch(DataType&& ptr)       // Item 25 explains
    : pw(std::move(ptr)) {}                     // use of std::move
    
    bool operator()() const
    { return pw->isValidated() && pw->isArchived(); }
    
private:
    DataType pw;
};

auto func = IsValAndArch(std::make_unique<Widget>());
bool bRet = func();

如果非要使用lambda表达式,且想实现移动捕获怎么办?答案就是:

  1. 把想要捕获的对象移动到std::bind产生的函数对象中;
  2. 给 lambda 表达式一个被捕获对象的引用;

看下面的例子:

std::vector<double> data;                       // as above// as above

auto func =
    std::bind(                                  // C++11 emulation
        [](const std::vector<double>& data)     // of init capture
        { /* uses of data */ },
        std::move(data)
);

和 lambda 表达式类似,std::bind产生一个函数对象。我们称std::bind返回的函数对象为绑定对象(bind objects)。std::bind的第一个参数是可调用对象,紧接着的所有实参表示表示传递给该对象的值。

返回的绑定对象含有传给std::bind的所有实参的副本。如果实参是左值,在绑定对象内对应的对象是通过拷贝构造的;如果是右值,则对应的内部对象是通过移动构造的。在上面的例子中,传给std::bind的第二个参数是右值(the result of std::move—see Item 23),所以data是被移动构造进绑定对象的。而这正是绕过C++ 11无法将右值移入闭包的受法。

当这个绑定对象被调用(即,调用它的函数调用运算符)时,它存储的实参被传给最初传给std::bind的可调用对象。在本例中的话,当func(绑定对象)被调用时,func内的由移动构造的data的副本就会作为实参传给那个最初传给std::bind的lambda式。

上例中的lambda多了一个data形参,对应我们的伪移动捕获对象。这个形参是绑定对象中的data副本的左值引用(而不是右值引用,虽然初始化data副本的表达式是std::move(data),但data的副本本身是一个左值)。如此一来,lambda内对data的操作,都会操作在绑定对象中移动构造而得来的data的副本之上。

默认地,lambda生成的闭包类中的operator()成员函数是带有const的。但是绑定对象内移动构造得到的data副本并不带const。为了防止data副本被以为修改,所以lambda表达式的形参也带上了const修饰符。如果lambda表达式被声明为mutable,则闭包里的oprator()就不会带有const,那么相应地,lambda表达式形参中的const就该省略了:

auto func =
    std::bind(                                      // C++11 emulation
        [](std::vector<double>& data) mutable       // of init capture
        { /* uses of data */ },                     // for mutable lambda
        std::move(data)
    );

总结一下:

  • 无法将一个对象移动构造进C++ 11的闭包里,但可以将一个对象移动构造进std::bind所产生的绑定对象中;
  • 在C++ 11中模拟移动捕获的步骤是:先将预捕获对象移动构造进一个绑定对象,然后按引用将该移动构造的对象传给lambda;
  • 因为绑定对象和闭包的生命周期相同,所以可以像对待闭包一样对待绑定对象中的对象;

再给一个前面 C++14 std::unique_ptr 移动到 lambda 闭包的例子:

auto func = [pw = std::make_unique<Widget>()]   // as before,
            { return pw->isValidated()          // create pw
                     && pw->isArchived(); };    // in closure

下面是c++ 11的模拟代码:

auto func = std::bind(
                [](const std::unique_ptr<Widget>& pw)
                { return pw->isValidated()
                      && pw->isArchived(); },
                std::make_unique<Widget>()
                );

Things to Remember

  • 使用c++ 14的init捕获将对象移动到闭包中;
  • 在c++ 11中,通过手工编写的类或std::bind模拟初始化捕获;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值