深入理解c 中的Lambda表示式

2021-10-01 22:28:51 字數 3302 閱讀 4317

lambda簡介

lambda表示式最重要的特點就是能夠極其方便地建立函式物件。

其實,lambda表示式能做到的事情,手工都能做到,無非就是多打一些字。

但是,lambda表示式提供的簡潔、易用、功能之強大,真是香啊!

總的來說,lambda表示式經常用於以下場景:

注意點:

int x;

auto c1 =

[x](

int y)

;auto c2 = c1;

// c2是c1的副本

auto c3 = c2;

// c3是c2的副本

// c1,c2,c3都是同一lambda式產生的閉包的副本

本文不再介紹基本使用,而關注使用經驗,如預設捕獲可能帶來的問題、有哪些好的實踐及與std::bind的對比等,關於lambda表示式的基礎知識,請參考 c++中的lambda表示式

避免預設捕獲

先說結論,按引用或按值的預設捕獲都可以導致使用空懸的引用,導致程式未定義的行為!

按引用預設捕獲

按引用捕獲會導致閉包包含對區域性變數或定義lambda式的作用域內(即定義lambda式的那個函式)的形參的引用。

一旦閉包越過了該區域性變數或形參的作用域,它們就會析構,閉包內對它們的引用就會空懸。

舉例來說,如下**:

// 乙個新增除數篩選器的函式

void

adddivisorfilter()

// 區域性變數按引用預設捕獲);

}

注意,預設捕獲了對區域性變數divisor的引用,當函式走到右大括號時,區域性變數析構,而篩選器對函式物件還在傻傻地執行著,但引用已經空懸!

其實,把引用預設捕獲修改為&divisor這樣顯式捕獲時,依然還會存在那個問題。但這樣大概能夠直接看到這個區域性變數在函式返回時就拜拜了,更容易查詢問題。

那要怎麼辦呢,其實也簡單,針對本例而言,把按引用的捕獲修改為按值的捕獲就行了。傳入了閉包區域性變數的副本,區域性變數析構時對它不會有影響。

按值的預設捕獲

但按值捕獲也非萬能之策。

考慮如下**:

class

foo;

void foo::

addfilter()

const

// 按值的預設捕獲,能通過編譯,但是去掉=號或者直接寫divisor捕獲,則編譯錯誤);

}

如果有這個疑問:divisor並不是在建立lambda式的作用域內可見的非靜態區域性變數或形參,它是怎麼通過編譯的?就要了解一下this裸指標的隱式應用了。

類內的每乙個非靜態成員函式都持有乙個this,每當使用類的成員變數時,實際上使用的是this->localvarname

所以在成員函式內按值預設捕獲,就捕獲了this。使用的也是this->divisor,而不是單個變數。那麼,既然是這樣,this的生命期就重要了,它可不能比閉包短哦。

但下面的**就沒有那麼幸運了:

void

dosomework()

如注釋所言,但是當該函式結束時,pf也就要銷毀了,this將不復存在,啊,filters中就含有了空懸指標的元素。只能加班除錯了。

怎麼解決這個問題呢?

簡單的方法是,在addfilter函式中先建立乙個divisor的區域性變數,再按值捕獲即可。這規避了捕獲this的問題。

c++14中有更好更直接的方法,就是廣義lambda捕獲。如下:

// c++14

void foo::

addfilter()

const

// 廣義lambda捕獲,把等號右側的變數賦值給左側變數,左側變數作為閉包的引數,不會空懸);

}

完美!

注意,雖然lambda表示式只能捕獲建立lambda式的作用域內可見的非靜態區域性變數或形參,但lambda式內依然可以使用靜態儲存期物件。按值捕獲可能會產生誤解。

如下**改自上述示例:

// 乙個新增除數篩選器的函式

void

adddivisorfilter()

// 區域性變數按值預設捕獲,實際上未捕獲任何變數,因為divisor是static的);

++divisor;

}

你以為divisor是按值捕獲,所以閉包內是它的副本,它永遠正確不會改變。

但你以為的是你以為的,不是我以為的。你只是使用了static的divisor,按值的預設捕獲未捕獲任何東西!所以注意到最後一行的自增操作,你的程式每執行一次,divisor都會不一樣哦!哈哈,又要加班除錯了。

所以,盡量不要使用預設捕獲模式。

c++14中的初始化捕獲

c++14中不再支援預設捕獲,而是改為初始化捕獲(即前文使用過的廣義lambda捕獲)。使用初始化捕獲,有以下特點:

示例如下:

class

fooauto func =

[pf = std::make_unique_ptr()

];// 初始化捕獲支援表示式

其中,捕獲語句中:

與使用c11的lambda式經常出現的問題相比,c14無疑是更好的選擇。

優先選用lambda式而非std::bind

兩者對比:

使用bind的場合:

class

polyfoo

polyfoo pf;

auto boundpf = std::

bind

(pf, _1)

;// 可以通過任意型別的實參呼叫

// boundpf(200);

// boundpf(nullptr);

// boundpf("abc");

其實,上述兩種使用場合在c14中已經不復存在:

auto boundpf =

[pf]

(const

auto

& param)

;

總之,在現代化的**中,盡量lambda表示式會帶來很多好處。

總結lambda表示式使用中,會有一些陷阱,需要注意,不要使用預設的捕獲。

c14中,使用初始化捕獲,不再支援預設捕獲,這是乙個非常有益的變動,推薦使用。

在c14中,lambda表示式基本可以取代難懂的bind,並更有可能獲得高效的執行**。

參考資料

《effective modern c++》

深入理解lambda

lambda 表示式 lambda expression 是乙個匿名函式,lambda表示式基於數學中的 演算得名,直接對應於其中的lambda抽象 lambda abstraction 是乙個匿名函式,即沒有函式名的函式。語法lambda 函式的語法只包含乙個語句,如下 lambda arg1 a...

深入理解Lambda

lambda是乙個表示式,也可以說它是乙個匿名函式。然而在使用它或是閱讀lambda 的時候,卻顯得並不那麼容易。因為它匿名,因為它刪減了一些必要的說明資訊 比如方法名 下面就來說說lambda是如何進行轉換和工作的吧。變數作用域說明 lambda評價 reflambda arg1 arg2,arg...

深入理解Lambda函式及其用法

1.函式式程式設計 例如 乙個整數列表,要求按照列表中元素的絕對值大小公升序排列 list1 3,5,4,1,0,2,6 sorted list1,key lambda x abs x 0,1,2,3,4,5,6 排序函式sorted支援接收乙個函式作為引數,該引數作為 sorted的排序依據,這裡...