第18課 捕獲機制及陷阱

一. lambda的捕獲方式ios

(一)3種捕獲方式:編程

  1. 按值捕獲:  [=]或[var],前者爲按值的默認捕獲方式閉包

  2. 按引用捕獲:[&]或[&var],前者爲按引用的默認捕獲方式函數

  3. 初始化捕獲(C++14):  見後面的《廣義捕獲》及由其引伸出來的移動捕獲功能。這種捕獲方式能夠作到C++11中全部捕獲方式可以作到的全部事情this

(二)默認捕獲方式的陷阱[=]和[&]spa

  1.按引用捕獲會致使閉包(由lambda表達式建立的對象)中包含指向局部對象或形參的引用。一旦該閉包超過該局部變量或形參的生命期,那麼閉包內的引用就會發生「引用懸空」。固然若是閉包和局部變量/形參的生命期相同,就不會出現這個問題指針

  2.按值的默認捕獲極易受懸空指針影響(尤爲是this),而且會讓人產生lambda表達式是獨立的、不受外界影響的錯覺code

【編程實驗】默認捕獲方式的陷阱對象

#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捕獲blog

(一)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;
}
相關文章
相關標籤/搜索