一. 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; }