• 第18课 捕获机制及陷阱


    一. lambda的捕获方式

    (一)3种捕获方式:

      1. 按值捕获:  [=]或[var],前者为按值的默认捕获方式

      2. 按引用捕获:[&]或[&var],前者为按引用的默认捕获方式

      3. 初始化捕获(C++14):  见后面的《广义捕获》及由其引申出来的移动捕获功能。这种捕获方式可以做到C++11中所有捕获方式能够做到的所有事情

    (二)默认捕获方式的陷阱[=]和[&]

      1.按引用捕获会导致闭包(由lambda表达式创建的对象)中包含指向局部对象或形参的引用。一旦该闭包超过该局部变量或形参的生命期,那么闭包内的引用就会发生“引用悬空”。当然如果闭包和局部变量/形参的生命期相同,就不会出现这个问题

      2.按值的默认捕获极易受悬空指针影响(尤其是this),并且会让人产生lambda表达式是独立的、不受外界影响的错觉

    【编程实验】默认捕获方式的陷阱

    #include <iostream>
    #include <vector>
    #include <memory>
    #include <functional>
    
    using namespace std;
    
    using FilterContainer = std::vector < std::function<bool(int)>>; //筛选函数的容器
    FilterContainer filters;
    
    //1. 按引用捕获造成的“引用悬空”问题
    void addDivisorFilter()
    {
        auto divisor = 10; //divisor可以是其它表达式通过运算的结果。
    
        //将跟这个除数(divisor)相关的过滤函数添加到vector中。
        //1.1 “引用悬空”问题
        filters.emplace_back(
            [&](int value) {return value % divisor == 0;} //由于divisor是个局部变量,被按引用捕获。当addDivisorFilter
                                                          //函数结束后,divisor被销毁。filters中相应的该闭包对象就存在
                                                          //一个绑定到被销毁变量的引用,形为“引用悬空”。由于这里是默
                                                          //认方式的按引用捕获,会捕获到所有的局部变量或形参,当lambda
                                                          //表达式与局部变量的生命期不同时,由于捕获到变量众多,很容易
                                                          //一不小心使用到这些“悬空”的引用。
        ); 
    
        //1.2 解决方案
        //filters.emplace_back(
        //    [=](int value) {return value % divisor == 0; } //ok, 这里改成按值捕获,则lambda中的divisor是局部变量的副本。
        //);
    }
    
    //2. 按值捕获this指针造成的“指针悬空”现象
    class Widget
    {
        int divisor;
    public:
        Widget(int div = 5):divisor(div){}
    
        void addFilter() const
        {
            //2.1 可能存在空悬指针现象
            filters.emplace_back(
                [=](int value) {return value % divisor == 0; } //注意这里的divisor是成员变量,不能被捕获。
                                                               //当按值捕获时,它是通过this指针来访问的。
                                                               //即divisor的生命期依赖于this所指对象本身。
            );
    
            //2.2 解决方案:复制divisor的副本到lambda中。
            auto divisorCopy = divisor;
    
            filters.emplace_back(
                [divisorCopy](int value) {return value % divisorCopy == 0; } //捕获divisor的副本。
            );
        }
    };
    
    void doSomeWork()
    {
        auto pw = std::make_unique<Widget>(5);
       
        //...     //做些其它事情
    
        pw->addFilter(); //当doSomeWork函数结束后,由于智能指针会自动释放widget对象。
                         //由于Widget对象的生命期比filters中相应的元素(lambda表达式)生命期短。
                         //因此,filters中就含有一个带有空悬指针的元素。
    }
    
    //3. 按值捕获的表达式并不完全独立(lambda可能依赖外部的静态变量)
    void ByValDependentcy()
    {
        static auto divisor = 10;
        filters.emplace_back(
            [=](int value) {return value % divisor == 0; }  //由于static无法被捕获,lambda是直接使用该
                                                            //变量的。
        );
    
        ++divisor;  //意外修改了divisor。上述的lambda中的[=],由于按值捕获会给人造成lambda式是独立的错觉。
                    //实际上该lambda中是直接使用static变量的,其值会随着ByValDependentcy函数的调用而逐次
                    //递增。这与按值默认捕获所暗示的含义直接相矛盾。解决的方案:不要采用按值默认的捕获
                    //方案,取而代之的是采用广义捕获(见后面的《广义捕获》内容)
    }
    
    int main()
    {
        return 0;
    }

    二. 初始化捕获也称为广义lambda捕获

    (一)C++11中捕获机制的局限

      1. lambda捕获的是局部变量或形参,不管是按值还是按引用捕获这些都是具名的左值对象。而右值对象是匿名对象,无法被捕获

      2. 按值捕获时,左值是被复制到闭包中的。如果被捕获的对象是个只移动类型的对象时,因其无法被复制,就会出错。此外,如果被捕获的对象如果是一个占用内存较大的对象时,按值捕获显然效率很低。

    (二)初始化捕获(C++14)

      1. 格式:形如[mVar1 = localVar1, mVar2 = std::move(localVar2)](){};

      2. 说明:

      (1)mVar1和mVar2是闭包类的成员变量的名字而位于“=”右侧的则是初始化表达式。

      (2)mVar1和mVar2的作用域就是闭包类的作用域即仅限于闭包类部可用。而“=”右侧的作用域则与该lambda表达式加以定义之处的作用域(即lambda式的父作用域)相同

      (3)初始化捕获使得可以在闭包类中指定成员变量的名字,以及使用表达式来初始化这些成员变量。

    (三)利用初始化捕获来实现移动捕获功能

      1. C++14中的移动捕获(通过初始化捕获将对象移入闭包)

      2. C++11中通过类或std::bind模拟移动捕获(std::bind模拟的手法)

      (1)将lambda表达式绑定到std::bind函数对象中,同时将需要捕获的对象移动到std::bind对象中。

      (2)在lambda表达式中通过引用绑定到要“捕获”的对象上。

    【编程实验】初始化捕获及移动对象

    #include <iostream>
    #include <memory>
    #include <functional>
    #include <vector>
    
    using namespace std;
    
    //1. 通过初始化捕获将对象移动闭包中
    class Widget
    {
    public:
        bool isValidated() const { return true; } //是否有效
        bool isProcessed() const { return true; } 
        bool isArchived() const { return true; }  //是否存档
    };
    
    //2. 使用类来模拟移动对象
    class IsValAndArch
    {
        using DataType = std::unique_ptr<Widget>;
        DataType mPW;
    
    public:
    
        //移动构造函数
        explicit IsValAndArch(DataType&& ptr):mPW(std::move(ptr))
        {
        }
    };
    
    int main()
    {
        auto pw = std::make_unique<Widget>(); //创建Widget对象
    
        //1. 通过初始化捕获将对象移动闭包中
        //1.1 使用std::move将pw这个只移动对象移入闭包中(C++14)
        auto func1 = [mPW = std::move(pw)]{ return mPW->isValidated() && mPW->isArchived(); };
        //1.2 可以在捕获列表中直接用表达式来初始化mPW成员变量。
        auto func2 = [mPW =  std::make_unique<Widget>()]{ return mPW->isValidated() && mPW->isArchived(); };
    
        //2.使用类模拟将只移动对象移入仿函数中
        auto func3 = IsValAndArch(std::make_unique<Widget>()); //仿函数对象,其中的unique是个只移动对象。
    
        //3.通过std::bind模拟初始化捕获
        std::vector<double> data;
        //3.1 使用初始化捕获,实现容易
        auto func4 = [data = std::move(data)]{}; //将data对象移入闭包中。
        
        //3.2 std::bind模拟移动对象到lambda中
        //注意事项:
        //(1)bind对象(即func5)会将所有实参的副本保存其中。对于左值会实施复制构造,对于右值会实施移
        //     动构造。因此,bind对象中,保留了第1个实参lambda和第2个实参data的副本,而第2个实参是通过
        //     std::move移动到bind对象中,成为其中的一个成员变量(模拟将data移入绑定对象)
        //(2)当调用func5时,该函数会将上述的data副本作为实参传递给其中的lambda表达式。
        //(3)lambda表达式的形参data是个引用,它是个指向func5对象中的data的左值引用(注意不是右值,因
        //     为在func5中副本本身是一个左值)。因此lambda对data的操作,都会实施在func5对象的data副本身上。
        //     由于lambda的operator()是个const函数,因此其内的成员变量都带有const属性。但bind对象上的data
        //     副本并不带const修饰符。为了防止该data副本在lambda中被修改,将lambda的形参声明为常量引用。
        //(4)由于bind对象存储所有实参的副本,因此bind对象中的lambda表达式也是一个副本,其生命期与bind对象一致。
        auto func5 = std::bind(
                              [](const std::vector<double>& data) {}, //第1个参数:lambda表达式,
                                 std::move(data)  //第2个参数,将data移动到bind对象中
                               );
    
        return 0;
    }
  • 相关阅读:
    Codeforces Round #590 D. Distinct Characters Queries
    线段树模板加模板题POJ3468
    hihoCoder挑战赛5 C 与链
    HDU 5044 Tree 树链剖分
    HYSBZ 1901 Dynamic Rankings 树状数组套主席树
    POJ 2761 Feed the dogs 主席树
    POJ 2104 K-th Number 主席树 区间第K大
    HDU 4547 CD操作 LCA
    POJ 1470 Closest Common Ancestors 离线LCA
    HYSBZ 1036 树的统计Count 树链剖分 线段树
  • 原文地址:https://www.cnblogs.com/5iedu/p/11390495.html
Copyright © 2020-2023  润新知