Effective Modern C++:06lambda表达式
???????? lambda表達(dá)式實(shí)際上是語法糖,任何lambda表達(dá)式能做到的,手動(dòng)都能做到,無非是多打幾個(gè)字。但是lambda作為一種創(chuàng)建函數(shù)對(duì)象的手段,實(shí)在太過方便,自從有了lambda表達(dá)式,使用復(fù)雜謂詞來調(diào)用STL中的”_if”族算法(std::find_if,std::remove_if等)變得非常方便,這種情況同樣發(fā)生在比較函數(shù)的算法族上。在標(biāo)準(zhǔn)庫(kù)之外,lambda表達(dá)式可以臨時(shí)制作出回調(diào)函數(shù)、接口適配函數(shù)或是語境相關(guān)函數(shù)的特化版本以供一次性調(diào)用。下面是關(guān)于lambda相關(guān)術(shù)語的提醒:
???????? lambda表達(dá)式,是表達(dá)式的一種,比如下面代碼中紅色的就是lambda表達(dá)式:
std::find_if(container.begin(), container.end(), [](int val) { return 0 < val && val < 10; });?? ? ? ? ?閉包,是lambda表達(dá)式創(chuàng)建的運(yùn)行期對(duì)象,在上面對(duì)std::find_if的調(diào)用中,閉包就是作為第三個(gè)實(shí)參在運(yùn)行期傳遞給std::find_if的對(duì)象。
???????? 閉包類,是實(shí)例化閉包的類,每個(gè)lambda表達(dá)式都會(huì)觸發(fā)編譯器生成一個(gè)獨(dú)一無二的閉包類,而lambda表達(dá)式中的語句會(huì)變成閉包類成員函數(shù)的可執(zhí)行指令。
????????
閉包可以復(fù)制,所以,對(duì)應(yīng)于單獨(dú)一個(gè)lambda表達(dá)式的閉包類型可以有多個(gè)閉包:
int x; // x is local variable auto c1 = [x](int y) { return x * y > 55; }; // c1 is copy of the closure produced by the lambda auto c2 = c1; // c2 is copy of c1 auto c3 = c2; // c3 is copy of c2 …?? ? ? ? ?c1、c2和c3都是同一個(gè)lambda表達(dá)式產(chǎn)生的閉包的副本。
???????? 在非正式場(chǎng)合,lambda表達(dá)式,閉包和閉包類之間的界限可以模糊一些。但是在下面的條款中,需要能區(qū)別哪些存在于編譯期(lambda表達(dá)式和閉包類),哪些存在于運(yùn)行期(閉包),以及它們之間的相互聯(lián)系。
?
31:避免默認(rèn)捕獲模式
???????? C++11中有兩種默認(rèn)捕獲模式:按引用或按值。按引用的默認(rèn)捕獲模式可能導(dǎo)致空懸引用,而按值的默認(rèn)捕獲模式可能會(huì)讓你覺得不存在空懸引用的問題(實(shí)際上不是)。
???????? 按引用捕獲會(huì)導(dǎo)致閉包包含指向局部變量(或形參)的引用,一旦由lambda表達(dá)式所創(chuàng)建的閉包的生命期超過了該局部變量或形參的生命期,那么閉包內(nèi)的引用就會(huì)空懸,比如下面的代碼:
using FilterContainer = std::vector<std::function<bool(int)>>; FilterContainer filters; // filtering funcsvoid addDivisorFilter() {auto calc1 = computeSomeValue1();auto calc2 = computeSomeValue2();auto divisor = computeDivisor(calc1, calc2);filters.emplace_back([&](int value) { return value % divisor == 0; }); }?? ? ? ? ?這段代碼隨時(shí)會(huì)出錯(cuò),lambda中按引用捕獲了局部變量divisor,但當(dāng)addDivisorFilter函數(shù)返回時(shí)局部變量被銷毀,使用filters就會(huì)產(chǎn)生未定義行為。
?
???????? 如果不這樣做,使用顯式方式按引用捕獲divisor,問題依然存在:
filters.emplace_back([&divisor](int value) { return value % divisor == 0; } );?? ? ? ? ?但是通過顯示捕獲,就比較容易看出lambda表達(dá)式的生存依賴于divisor的生命期。顯式的寫出”divisor”可以提醒我們要保證divisor至少應(yīng)該和lambda具有一樣長(zhǎng)的生命期,這要比[&]這種所傳達(dá)的不痛不癢的“要保證沒有空懸引用”式的勸告更讓人印象深刻。
?
???????? 如果知道閉包會(huì)立即使用(比如傳遞給STL算法)且不會(huì)被復(fù)制,這種情況下,你可能會(huì)爭(zhēng)論說,既然沒有空懸引用的風(fēng)險(xiǎn),也就沒有必要避免使用默認(rèn)引用捕獲模式。但是從長(zhǎng)遠(yuǎn)觀點(diǎn)來看,顯示的列出lambda表達(dá)式所依賴的局部變量或形參,是更好的軟件工程實(shí)踐。
?
???????? 上面的例子中,解決問題的一種辦法是對(duì)divisor采用按值的默認(rèn)捕獲模式:
filters.emplace_back([=](int value) { return value % divisor == 0; } );?? ? ? ? ?對(duì)于這個(gè)例子而言,這樣做確實(shí)是沒問題的。但是按值的默認(rèn)捕獲并非一定能避免空懸引用,問題在于如果按值捕獲了一個(gè)指針,在lambda表達(dá)式創(chuàng)建的閉包中持有的是這個(gè)指針的副本,但是沒有辦法阻止lambda表達(dá)式之外的代碼針對(duì)該指針實(shí)施delete操作導(dǎo)致的指針副本空懸。比如下面的代碼:
class Widget { public:… // ctors, etc.void addFilter() const; // add an entry to filters private:int divisor; // used in Widget's filter };void Widget::addFilter() const {filters.emplace_back([=](int value) { return value % divisor == 0; }); }?? ? ? ? ?這樣的代碼看起來安全,然而實(shí)際上卻是大錯(cuò)特錯(cuò)的。捕獲只能針對(duì)于在創(chuàng)建lambda表達(dá)式的作用域內(nèi)可見的非靜態(tài)局部變量(包括形參),而在Widget::addFilter函數(shù)體內(nèi),divisor并非局部變量,而是Widget類的成員變量,它根本沒辦法捕獲。這么一來,如果不使用默認(rèn)捕獲模式,代碼就不會(huì)通過編譯:
void Widget::addFilter() const {filters.emplace_back([](int value) { return value % divisor == 0; }); }?? ? ? ? ?而且,如果試圖顯示捕獲divisor(無論是按值還是按引用),這個(gè)捕獲語句都不能通過編譯,因?yàn)閐ivisor既不是局部變量,也不是形參:
void Widget::addFilter() const {filters.emplace_back([divisor](int value) { return value % divisor == 0; }); }?? ? ? ? ?但是為什么一開始的代碼沒有發(fā)生編譯錯(cuò)誤呢?this指針是關(guān)鍵所在,每一個(gè)非靜態(tài)成員函數(shù)都持有一個(gè)this指針,每當(dāng)提及該類的成員變量時(shí)都會(huì)用到這個(gè)指針。比如在Widget的任何成員函數(shù)中,編譯器內(nèi)部都會(huì)把divisor替換成this->divisor。因此,在Widget::addFilter的按值默認(rèn)捕獲版本中,被捕獲的實(shí)際上是Widget的this指針,而不是divisor。從編譯器的角度來看,實(shí)際的代碼相當(dāng)于:
void Widget::addFilter() const {auto currentObjectPtr = this;filters.emplace_back([currentObjectPtr](int value) { return value % currentObjectPtr->divisor == 0; }); }?? ? ? ? ?因此,該lambda閉包的存活,與它含有this指針指向的Widget對(duì)象的生命期是綁在一起的,比如下面的代碼:
using FilterContainer = std::vector<std::function<bool(int)>>; FilterContainer filters; void doSomeWork() {auto pw = std::make_unique<Widget>(); pw->addFilter();… }?? ? ? ? ?當(dāng)調(diào)用doSomeWork時(shí)創(chuàng)建了一個(gè)篩選函數(shù),它依賴于std::make_unique創(chuàng)建的Widget對(duì)象,該函數(shù)被添加到filters中,然而當(dāng)doSomeWork結(jié)束后,Widget對(duì)象隨著std::unique_ptr的銷毀而銷毀,從那一刻起,filters中就含有了一個(gè)帶有空懸指針的元素。
???????? 這一問題可以通過將想捕獲的成員變量復(fù)制到局部變量中,而后捕獲該局部變量的部分得意解決:
void Widget::addFilter() const {auto divisorCopy = divisor;filters.emplace_back([divisorCopy](int value) { return value % divisorCopy == 0; } ); }??
???????? 在C++14中,捕獲成員變量的一種更好的方式是使用廣義lambda捕獲(generalized lambda):
void Widget::addFilter() const {filters.emplace_back([divisor = divisor](int value) { return value % divisor == 0; }); }?? ? ? ? ?對(duì)廣義lambda捕獲而言,沒有默認(rèn)捕獲模式一說,但是,就算在C++14中,本條款的建議,避免使用默認(rèn)捕獲模式依然成立。
?
???????? 使用按值默認(rèn)捕獲的另一個(gè)缺點(diǎn),在于它似乎表明閉包是自治的,與閉包外的數(shù)據(jù)變化絕緣,然而作為一般性的結(jié)論,這是不正確的。因?yàn)閘ambda表達(dá)式可能不僅依賴于局部變量或形參,他還可能依賴于靜態(tài)存儲(chǔ)期對(duì)象,這樣的對(duì)象定義在全局或名字空間作用域中,或是在類,函數(shù),文件中以static飾詞聲明。這樣的對(duì)象可以在lambda內(nèi)使用,但是它們不能被捕獲。如果使用了按值默認(rèn)捕獲模式,這些對(duì)象就會(huì)給人以錯(cuò)覺,認(rèn)為它們可以加以捕獲:
void addDivisorFilter() {static auto calc1 = computeSomeValue1(); static auto calc2 = computeSomeValue2(); static auto divisor = computeDivisor(calc1, calc2);filters.emplace_back([=](int value) { return value % divisor == 0; });++divisor; }?? ? ? ? ?看到[=]就認(rèn)為lambda復(fù)制了它內(nèi)部使用的對(duì)象,得出lambda是自治的這種結(jié)論,是錯(cuò)誤的。實(shí)際上該lambda表達(dá)式并不獨(dú)立,它沒有使用任何的非靜態(tài)局部變量或形參,所以它沒能捕獲任何東西。更糟糕的是lambda表達(dá)式的代碼中使用了靜態(tài)變量divisor,每次調(diào)用addDivisorFilter后,divisor會(huì)遞增,使得添加到filters中的每個(gè)lambda表達(dá)式的行為都不一樣。如果一開始就避免使用按值的默認(rèn)捕獲模式,也就能消除代碼被誤讀的風(fēng)險(xiǎn)了。
?
?
32:使用初始化捕獲將對(duì)象移入閉包
???????? 有時(shí)按值捕獲和按引用捕獲并不能滿足所有的需求。比如想要把move-only對(duì)象(如std::unique_ptr或std::future)放入閉包,或者想把復(fù)制昂貴而移動(dòng)低廉的對(duì)象移入閉包時(shí),C++11沒有提供可行的方法,但是C++14為對(duì)象移動(dòng)提供了直接支持。
???????? 實(shí)際上,C++14提供了一種全新的捕獲方式,按移動(dòng)的捕獲只不過是該機(jī)制能夠?qū)崿F(xiàn)的多種效果之一罷了。這種方式稱為初始化捕獲(init capture),它可以做到C++11的捕獲形式所有能夠做到的事情(除了默認(rèn)捕獲模式,而這是需要遠(yuǎn)離的),不過初始化捕獲的語法稍顯啰嗦,如果C++11的捕獲能解決問題,則大可以使用之。
???????? 下面是初始化捕獲實(shí)現(xiàn)移入捕獲的例子:
class Widget { public:bool isValidated() const;bool isProcessed() const;bool isArchived() const; private:… }; auto pw = std::make_unique<Widget>(); … auto func = [pw = std::move(pw)] { return pw->isValidated() && pw->isArchived(); };?? ? ? ? ?上面的例子中,位于”=”左側(cè)的pw,是lambda創(chuàng)建的閉包類中成員變量的名字;而位于”=”右側(cè)的是其初始化表達(dá)式,所以”pw=std::move(pw)”表達(dá)了在閉包類中創(chuàng)建一個(gè)成員變量pw,然后使用針對(duì)局部變量pw實(shí)施std::move的結(jié)果來初始化該成員變量。在lambda內(nèi)部使用pw也是指的閉包類的成員變量。一旦定義lambda表達(dá)式之后,因?yàn)榫植孔兞縫w已經(jīng)被move了,所以其不再掌握任何資源。
???????? 上面的例子還可以不使用局部變量pw:
auto func = [pw = std::make_unique<Widget>()]{ return pw->isValidated() && pw->isArchived(); };?? ? ? ? ?這種捕獲方式在C++14中還稱為廣義lambda捕獲(generalized lambda capture)。
?
???????? 但是如果編譯器尚不支持C++14,則該如何實(shí)現(xiàn)按移動(dòng)捕獲呢?要知道一個(gè)lambda表達(dá)式不過是生成一個(gè)類并創(chuàng)建一個(gè)該類的對(duì)象的手法罷了,并不存在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麻煩很多。
如果非要使用lambda實(shí)現(xiàn)按移動(dòng)捕獲,也不是全無辦法,可以借助std::bind實(shí)現(xiàn):把要捕獲的對(duì)象移動(dòng)到std::bind產(chǎn)生的函數(shù)對(duì)象中;給lambda表達(dá)式一個(gè)指向欲捕獲的對(duì)象的引用。比如C++14中的寫法:
std::vector<double> data; … // populate data auto func = [data = std::move(data)]{ /* uses of data */ };?? ? ? ? ?如果采用C++11中使用std::bind和lambda的寫法,等價(jià)代碼如下:
std::vector<double> data; … // as above auto func = std::bind([](const std::vector<double>& data) { /* uses of data */ },std::move(data) );?? ? ? ? ?std::bind也生成函數(shù)對(duì)象,可以將它生成的對(duì)象稱為綁定對(duì)象。std::bind的第一個(gè)實(shí)參是個(gè)可調(diào)用對(duì)象,接下來的所有實(shí)參表示傳遞給該對(duì)象的值。
???????? 綁定對(duì)象內(nèi)含有傳遞給std::bind所有實(shí)參的副本。對(duì)于左值實(shí)參,綁定對(duì)象內(nèi)對(duì)應(yīng)的副本實(shí)施的是復(fù)制構(gòu)造;對(duì)于右值實(shí)參,實(shí)施的是移動(dòng)構(gòu)造。上面的例子中,第二個(gè)實(shí)參是個(gè)右值,所以在綁定對(duì)象內(nèi),使用局部變量data移動(dòng)構(gòu)造其副本,這種移動(dòng)構(gòu)造動(dòng)作正是實(shí)現(xiàn)模擬移動(dòng)捕獲的關(guān)鍵所在,因?yàn)榘延抑狄迫虢壎▽?duì)象,正是繞過C++11無法將右值移動(dòng)到閉包的手法。
???????? 當(dāng)一個(gè)綁定對(duì)象被調(diào)用時(shí),它所存儲(chǔ)的實(shí)參會(huì)傳遞給std::bind的那個(gè)可調(diào)用對(duì)象,也就是func被調(diào)用時(shí),func內(nèi)經(jīng)由移動(dòng)構(gòu)造得到的data副本就會(huì)作為實(shí)參傳遞給那個(gè)原先傳遞給std::bind的lambda表達(dá)式。這個(gè)C++11寫法比C++14多了一個(gè)形參data,該形參是個(gè)指向綁定對(duì)象內(nèi)部的data副本的左值引用,這么一來,在lambda內(nèi)對(duì)data形參所做的操作,都會(huì)實(shí)施在綁定對(duì)象內(nèi)移動(dòng)構(gòu)造而得的data副本之上,與原局部變量data無關(guān)。
?
???????? 默認(rèn)情況下,lambda閉包類中的operator()成員函數(shù)會(huì)帶有const飾詞,因此閉包里的所有成員變量在lambda表達(dá)式的函數(shù)體內(nèi)都帶有const飾詞,但綁定對(duì)象內(nèi)移動(dòng)構(gòu)造而得的data副本并不帶有const飾詞,所以為了防止該data部分在lambda表達(dá)式內(nèi)被意外修改,lambda的形參就聲明為常量引用。但是如果lambda表達(dá)式帶有mutable飾詞,則閉包中的operator()函數(shù)就不會(huì)在聲明時(shí)帶有const飾詞,相應(yīng)的做法就是在lambda聲明中略去const:
auto func = std::bind([](std::vector<double>& data) mutable { /* uses of data */ },std::move(data) );?? ? ? ? ?綁定對(duì)象存儲(chǔ)著傳遞給std::bind所有實(shí)參的副本,因此本例中的綁定對(duì)象就包含一份由第一個(gè)實(shí)參lambda表達(dá)式產(chǎn)生的閉包的副本。這么一來,該閉包的生命期就和綁定對(duì)象是相同的。
?
???????? 另外一個(gè)例子,下面是C++14的代碼:
auto func = [pw = std::make_unique<Widget>()] { return pw->isValidated() && pw->isArchived(); };?? ? ? ? ?如果使用C++11采用bind的寫法:
auto func = std::bind([](const std::unique_ptr<Widget>& pw){ return pw->isValidated() && pw->isArchived(); },std::make_unique<Widget>() );??
?
33:要對(duì)auto&&類型的形參使用std::forward,則需要使用decltype
???????? 泛型lambda表達(dá)式(generic lambda)是C++14最振奮人心的特性之一:lambda表達(dá)式的形參列表中可以使用auto,它的實(shí)現(xiàn)直截了當(dāng),閉包類中的operator()采用模板實(shí)現(xiàn)。比如下面的lambda表達(dá)式,以及其對(duì)應(yīng)的實(shí)現(xiàn):
auto f = [](auto x){ return func(normalize(x)); };class SomeCompilerGeneratedClassName { public:template<typename T> auto operator()(T x) const{ return func(normalize(x)); }… };?? ? ? ? ?這個(gè)例子中,lambda表達(dá)式對(duì)x的動(dòng)作就是將其轉(zhuǎn)發(fā)給normalize,如果normalize區(qū)別對(duì)待左值和右值,則該lambda表達(dá)式的實(shí)現(xiàn)是有問題的,正確的寫法應(yīng)該是使用萬能引用并將其完美轉(zhuǎn)發(fā)給normalize:
auto f = [](auto&& x) { return func(normalize(std::forward<???>(x))); };?? ? ? ? ?這里的問題是,std::forward的模板實(shí)參”???”應(yīng)該怎么寫?
這里可以使用decltype(x),但是decltype(x)產(chǎn)生的結(jié)果,卻與std::forward的使用慣例有所不同。如果傳入的是個(gè)左值,則x的類型是左值引用,decltype(x)得到的也是左值引用;如果傳入的是右值,則x的類型是右值引用,decltype(x)得到的也是右值引用,但是,std::forward的使用慣例是std::forward<T>,其中T要么是個(gè)左值引用,要么是個(gè)非引用。
???????? 再看一下條款28中std::forward的簡(jiǎn)單實(shí)現(xiàn):
template<typename T> T&& forward(remove_reference_t<T>& param) {return static_cast<T&&>(param); }?? ? ? ? ?如果客戶代碼想要完美轉(zhuǎn)發(fā)Widget類型的右值,則按照慣例它應(yīng)該采用Wdiget類型,而非引用類型來實(shí)例化std::forward,然后std::forard模板實(shí)例化結(jié)果是:
Widget&& forward(Widget& param) { return static_cast<Widget&&>(param); }?? ? ? ? ?如果使用右值引用實(shí)例化T,也就是Widget&&實(shí)例化T,得到的結(jié)果是:
Widget&& && forward(Widget& param) {return static_cast<Widget&& &&>(param); }?? ? ? ? ?實(shí)施了引用折疊之后:
Widget&& forward(Widget& param) {return static_cast<Widget&&>(param); }?? ? ? ? ?經(jīng)過對(duì)比,發(fā)現(xiàn)這個(gè)版本和T為Widget時(shí)的std::forward是完全一樣的,因此,實(shí)例化std::forward時(shí),使用一個(gè)右值引用和使用非引用類型,結(jié)果是相同的。所以,我們的完美轉(zhuǎn)發(fā)lambda表達(dá)式如下:
auto f =[](auto&& param){return func(normalize(std::forward<decltype(param)>(param)));};?? ? ? ? ?稍加改動(dòng),就可以得到能接收多個(gè)形參的完美轉(zhuǎn)發(fā)lambda式版本,因?yàn)镃++14中的lambda能夠接受變長(zhǎng)形參:
auto f =[](auto&&... params){return func(normalize(std::forward<decltype(params)>(params)...));};?
??
34:優(yōu)先使用lambda表達(dá)式,而非std::bind
???????? std::bind在2005年就已經(jīng)是標(biāo)準(zhǔn)庫(kù)的組成部分了(std::tr1::bind),這意味著std::bind已經(jīng)存在了十多年了,你可能不太愿意放棄這么一個(gè)運(yùn)作良好的工具,然而有時(shí)候改變也是有益的,因?yàn)樵贑++11中,相對(duì)于std::bind,lambda幾乎總會(huì)是更好的選擇,而到了C++14,lambda簡(jiǎn)直已成了不二之選。
???????? lambda表達(dá)式相對(duì)于std::bind的優(yōu)勢(shì),最主要的是其具備更高的可讀性:
// typedef for a point in time (see Item 9 for syntax) using Time = std::chrono::steady_clock::time_point; // see Item 10 for "enum class" enum class Sound { Beep, Siren, Whistle }; // typedef for a length of time using Duration = std::chrono::steady_clock::duration; // at time t, make sound s for duration d void setAlarm(Time t, Sound s, Duration d);// setSoundL ("L" for "lambda") is a function object allowing a // sound to be specified for a 30-sec alarm to go off an hour // after it's set auto setSoundL = [](Sound s){// make std::chrono components available w/o qualificationusing namespace std::chrono;setAlarm(steady_clock::now() + hours(1), s, seconds(30));};?? ? ? ? ?這里的lambda表達(dá)式,即使是沒什么經(jīng)驗(yàn)的讀者也能看出來,傳遞給lambda的形參會(huì)作為實(shí)參傳遞給setAlarm。到了C++14中,C++14提供了秒,毫秒和小時(shí)的標(biāo)準(zhǔn)字面值,所以,可以寫成這樣:
auto setSoundL = [](Sound s) {using namespace std::chrono;using namespace std::literals;setAlarm(steady_clock::now() + 1h, s, 30s); };?? ? ? ? ?而下面的代碼是使用std::bind的等價(jià)版本,不過實(shí)際上它還有一處錯(cuò)誤的,后續(xù)在解決這個(gè)錯(cuò)誤:
using namespace std::chrono; using namespace std::literals; using namespace std::placeholders; // needed for use of "_1" auto setSoundB = // "B" for "bind"std::bind(setAlarm, steady_clock::now() + 1h, _1, 30s);?? ? ? ? ?對(duì)于初學(xué)者而言,占位符”_1”簡(jiǎn)直好比天書,而即使是行家也需要腦補(bǔ)出從占位符數(shù)字到它在std::bind形參列表中的位置映射關(guān)系,才能理解在調(diào)用setSoundB時(shí)傳入的第一個(gè)實(shí)參,會(huì)作為第二個(gè)實(shí)參傳遞給setAlarm。該實(shí)參的類型在std::bind的調(diào)用過程中是未加識(shí)別的,所以還需要查看setAlarm的聲明才能決定應(yīng)該傳遞何種類型的實(shí)參到setSoundB。
???????? 這段代碼的錯(cuò)誤之處在于,在lambda表達(dá)式中,表達(dá)式”steady_clock::now() + 1h”是setAlarm的實(shí)參之一,這一點(diǎn)清清楚楚,該表達(dá)式會(huì)在setAlarm被調(diào)用時(shí)求值,這樣是符合需求的,就是需要在setAlarm被調(diào)用的時(shí)刻之后的一個(gè)小時(shí)啟動(dòng)報(bào)警。但是在std::bind中,”steady_clock::now() + 1h”作為實(shí)參傳遞給std::bind,而非setAlarm,該表達(dá)式在調(diào)用std::bind時(shí)就進(jìn)行求值了,并且求得的結(jié)果會(huì)存儲(chǔ)在綁定對(duì)象中,這導(dǎo)致的結(jié)果是報(bào)警的啟動(dòng)時(shí)刻是在std::bind調(diào)用之后的一個(gè)小時(shí),而非setAlarm調(diào)用之后的一個(gè)小時(shí)。
???????? 要解決這個(gè)問題,就需要std::bind延遲表達(dá)式的求值到調(diào)用setAlarm的時(shí)刻,實(shí)現(xiàn)這一點(diǎn),就是需要嵌套第二層std::bind的調(diào)用:
auto setSoundB =std::bind(setAlarm,std::bind(std::plus<>(), steady_clock::now(), 1h),_1,30s);?? ? ? ? ?在C++14中,標(biāo)準(zhǔn)運(yùn)算符模板的模板類型實(shí)參大多數(shù)情況下可以省略不寫,所以此處也沒必要在std::plus中提供了,而C++11中還沒有這樣的特性,所以在C++11中,想要實(shí)現(xiàn)上面的代碼,只能是:
using namespace std::chrono; // as above using namespace std::placeholders; auto setSoundB =std::bind(setAlarm,std::bind(std::plus<steady_clock::time_point>(), steady_clock::now(), hours(1)),_1,seconds(30));??
???????? 如果對(duì)setAlarm實(shí)施了重載,則又會(huì)有新的問題:
enum class Volume { Normal, Loud, LoudPlusPlus }; void setAlarm(Time t, Sound s, Duration d, Volume v); auto setSoundL = [](Sound s) {using namespace std::chrono;setAlarm(steady_clock::now() + 1h, s, 30s); };?? ? ? ? ?即使有了重載,lambda表達(dá)式依然能正常工作,重載決議會(huì)選擇有三個(gè)參數(shù)版本的setAlarm。但是到了std::bind,就沒辦法通過編譯了:
auto setSoundB = std::bind(setAlarm, std::bind(std::plus<>(),steady_clock::now(),1h),_1,30s);?? ? ? ? ?這是因?yàn)榫幾g器無法確定應(yīng)該將哪個(gè)setalarm傳遞給set::bind,它拿到的所有信息只有一個(gè)函數(shù)名。為了使std::bind能夠通過編譯,setAlarm必須強(qiáng)制轉(zhuǎn)換到適當(dāng)?shù)暮瘮?shù)指針類型:
using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d); auto setSoundB = std::bind(static_cast<SetAlarm3ParamType>(setAlarm),std::bind(std::plus<>(), steady_clock::now(), 1h),_1,30s);?? ? ? ? ?但是這么做又帶來了lambda和std::bind的另一個(gè)不同之處。在lambda生成的setSoundL的函數(shù)調(diào)用運(yùn)算符中,調(diào)用setAlarm采用的是常規(guī)函數(shù)喚起方式,這么一來,編譯器就可以用慣常的手法將其內(nèi)聯(lián):
setSoundL(Sound::Siren); // body of setAlarm may well be inlined here?? ? ? ? ?而std::bind調(diào)用中使用了函數(shù)指針,這意味著在setSoundB的函數(shù)調(diào)用運(yùn)算符中,setAlarm是通過函數(shù)指針來調(diào)用的,編譯器一般無法將函數(shù)指針發(fā)起的函數(shù)調(diào)用進(jìn)行內(nèi)聯(lián),所以lambda表達(dá)式就有可能生成比std::bind更快的代碼。
?
???????? 在setAlarm例子中,僅僅涉及了函數(shù)的調(diào)用而已,如果你想做的事比這更復(fù)雜,則lambda表達(dá)式的優(yōu)勢(shì)則更加明顯。比如:
auto betweenL =[lowVal, highVal](const auto& val){ return lowVal <= val && val <= highVal; };?? ? ? ? ?這里的lambda使用了捕獲。std::bind要想要實(shí)現(xiàn)同樣的功能,必須用比較晦澀的方式來構(gòu)造代碼,下面分別是C++14和C++11的寫法:
using namespace std::placeholders; auto betweenB =std::bind(std::logical_and<>(),std::bind(std::less_equal<>(), lowVal, _1),std::bind(std::less_equal<>(), _1, highVal));auto betweenB = std::bind(std::logical_and<bool>(),std::bind(std::less_equal<int>(), lowVal, _1),std::bind(std::less_equal<int>(), _1, highVal));?? ? ? ? ?還是需要使用std::bind的延遲計(jì)算方法。
????????
???????? 再看下面的代碼:
enum class CompLevel { Low, Normal, High }; Widget compress(const Widget& w, CompLevel lev); //make compressedcopy of w Widget w; using namespace std::placeholders; auto compressRateB = std::bind(compress, w, _1);?? ? ? ? ?這里的w傳遞給std::bind時(shí),是按值存儲(chǔ)在std::bind生成的對(duì)象中的,在std::bind的調(diào)用中,按值還是按引用存儲(chǔ)只能是牢記規(guī)則。std::bind總是復(fù)制其實(shí)參,但是調(diào)用方可以通過對(duì)實(shí)參實(shí)施std::ref的方法達(dá)到按引用存儲(chǔ)的效果,因此:
auto compressRateB = std::bind(compress, std::ref(w), _1);?結(jié)果就是compressRateB的行為如同持有的是個(gè)指向w的引用,而非其副本。
而在lambda中,w無論是按值還是按引用捕獲,代碼中的書寫方式都很明顯:
auto compressRateL = [w](CompLevel lev) { return compress(w, lev); };?? ? ? ? ?同樣明顯的還有形參的傳遞方式:
compressRateL(CompLevel::High); // arg is passed by value compressRateB(CompLevel::High); // how is arg passed??? ? ? ? ?Lambda返回的閉包中,很明顯實(shí)參是按值傳遞給lev的;而在std::bind返回綁定對(duì)象中,形參的傳遞方式是什么呢?這里也只能牢記規(guī)則,綁定對(duì)象的所有實(shí)參都是按引用傳遞的,因?yàn)榇朔N對(duì)象的函數(shù)調(diào)用運(yùn)算符使用了完美轉(zhuǎn)發(fā)。
????????
???????? 總而言之,lambda表達(dá)式要比std::bind可讀性更好,表達(dá)能力更強(qiáng),運(yùn)行效率也可能更好,在C++14中,幾乎沒有std::bind的適當(dāng)用例,而在C++11中,std::bind僅在兩個(gè)受限場(chǎng)合還算有使用的理由:
???????? 移動(dòng)捕獲,C++11沒有提供移動(dòng)捕獲的語法,參考上一條款;
? ? ? ? ?多態(tài)函數(shù)對(duì)象,因?yàn)榻壎▽?duì)象的函數(shù)調(diào)用運(yùn)算符使用了完美轉(zhuǎn)發(fā),所以可以接收任何類型的實(shí)參,因此當(dāng)需要綁定的對(duì)象具有一個(gè)函數(shù)調(diào)用運(yùn)算符模板時(shí),是有利用價(jià)值的:
class PolyWidget { public:template<typename T>void operator()(const T& param);… };PolyWidget pw; auto boundPW = std::bind(pw, _1); boundPW(1930); // pass int to PolyWidget::operator() boundPW(nullptr); // pass nullptr to PolyWidget::operator() boundPW("Rosebud"); // pass string literal to PolyWidget::operator()?? ? ? ? ?C++11中的lambda表達(dá)式?jīng)]有辦法實(shí)現(xiàn)這一點(diǎn),但是在C++14中,使用帶有auto類型形參的lambda表達(dá)式可以很容易的實(shí)現(xiàn)這一點(diǎn):
auto boundPW = [pw](const auto& param) { pw(param); };?
轉(zhuǎn)載于:https://www.cnblogs.com/gqtcgq/p/9937013.html
總結(jié)
以上是生活随笔為你收集整理的Effective Modern C++:06lambda表达式的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: pymysql 模块 使用目录
- 下一篇: XV6第一个进程