021 lambda 表達式

我們可以向一個算法傳遞任何類別的可調(diào)用對象(callable object)。對于一個對象或個表達式鸯屿,如果可以對其使用調(diào)用運算符澈吨,則稱它為可調(diào)用的。即寄摆,如果 e 是一個可調(diào)用的表達式谅辣,則我們可以編寫代碼 e(args),其中 args 是一個逗號分隔的一個或多個參數(shù)的列表婶恼。

C++ 中可調(diào)用對象有函數(shù)桑阶、函數(shù)指針柏副、重載了函數(shù)調(diào)用運算符的類,以及 lambda 表達式(lambda expression)蚣录。

一個 lambda 表達式表示一個可調(diào)用的代碼單元割择。我們可以將其理解為一個未命名的內(nèi)聯(lián)函數(shù)。與任何函數(shù)類似包归,一個 lambda 具有一個返回類型锨推、一個參數(shù)列表和一個函數(shù)體。但與函數(shù)不同, lambda 可能定義在函數(shù)內(nèi)部公壤。一個 lambda 表達式具有如下形式:

[capture  list](parameter  list)->  return  type  {  function  body }

其中换可,capture list(捕獲列表)是一個 lambda 所在函數(shù)中定義的局部變量的列表(通常為空); return type、parameter list 和 function body 與任何普通函數(shù)一樣厦幅,分別表示返回類型沾鳄、參數(shù)列表和函數(shù)體。但是确憨,與普通函數(shù)不同译荞,lambda 必須使用尾置返回來指定返回類型。

我們可以忽略參數(shù)列表和返回類型休弃,但必須永遠包含捕獲列表和函數(shù)體:

auto f = [] {return 42;}

上面的例子中吞歼,我們定義了一個可調(diào)用對象 f,它不接受參數(shù)塔猾,返回 42篙骡。

lambda 的調(diào)用方式與普通函數(shù)的調(diào)用方式相同,都是使用調(diào)用運算符:

cout << f() << endl; // 打印 42

在 lambda 中忽略括號和參數(shù)列表等價于指定一個空參數(shù)列表丈甸。在此例中糯俗,當調(diào)用 f 時,參數(shù)列表是空的睦擂。如果忽略返回類型得湘,lambda 根據(jù)函數(shù)體中的代碼推斷出返回類型。如果函數(shù)體只是一個 return 語句顿仇,則返回類型從返回的表達式的類型推斷而來淘正。否則,返回類型為 void臼闻。

注意:如果 lambda 的函數(shù)體包含任何單一 return 語句之外的內(nèi)容跪帝,且未指定返回類型,則返回 void些阅。

向 lambda 傳遞參數(shù)

與一個普通函數(shù)調(diào)用類似伞剑,調(diào)用一個 lambda 時給定的實參被用來初始化 lambda 的形參。通常市埋,實參和形參的類型必須匹配黎泣。但與普通函數(shù)不同恕刘,lambda 不能有默認參數(shù)。因此抒倚,一個 lambda 調(diào)用的實參數(shù)目永遠與形參數(shù)目相等褐着。一旦形參初始化完畢,就可以執(zhí)行函數(shù)體了托呕。

一個帶參數(shù)的 lambda 的例子:

[](const  string  &a,  const  string  &b) { 
  return  a.size() < b.size();
}

空捕獲列表表明此 lambda 不使用它所在函數(shù)中的任何局部變量含蓉。

如下所示,可以使用此 lambda 來調(diào)用 stable_sort:

// 按長度排序,長度相同的單詞維持字典序
stable_sort(words.begin(),  words.end(),
[](const  string  &a,  const  string  &b) { return  a.size() < b.size();});

當 stable_sort 需要比較兩個元素時项郊,它就會調(diào)用給定的這個 lambda 表達式馅扣。

使用捕獲列表

雖然一個 lambda 可以出現(xiàn)在一個函數(shù)中,使用其局部變量着降,但它只能使用那些明確指明的變量差油。一個 lambda 通過將局部變量包含在其捕獲列表中來指出將會使用這些變量。捕獲列表指引 lambda 在其內(nèi)部包含訪問局部變量所需的信息任洞。

在本例中蓄喇,我們的 lambda 會捕獲 sz,并只有單一的 string 參數(shù)交掏。其函數(shù)體會將 string 的大小與捕獲的 sz 的值進行比較:

[sz](const  string  &a)
{ return  a.size() >=  sz; };

lambda 以一對 [] 開始妆偏,我們可以在其中提供一個以逗號分隔的名字列表,這些名字都是它所在函數(shù)中定義的盅弛。

由于此 lambda 捕獲 sz楼眷,因此 lambda 的函數(shù)體可以使用 sz。lambda 不捕獲 words 因此不能訪問此變量熊尉。如果我們給 lambda 提供一個空捕獲列表,則代碼會編譯錯誤:

// 錯誤:sz 未捕獲
[](const  string  &a) { return  a.size() >=  sz; };
注意:一個 lambda 只有在其捕獲列表中捕獲一個它所在函數(shù)中的局部變量掌腰,才能在函數(shù)體中使用該變量狰住。

lambda 捕獲和返回

當定義一個 lambda 時,編譯器生成一個與 lambda 對應(yīng)的新的(未命名的)類類型齿梁。我們可以這樣理解催植,當向一個函數(shù)傳遞一個 lambda 時,同時定義了一個新類型和該類型的一個對象:傳遞的參數(shù)就是此編譯器生成的類類型的未命名對象勺择。類似的创南,當使用 auto 定義一個用 lambda 初始化的變量時,定義了一個從 lambda 生成的類型的對象省核。

默認情況下稿辙,從 lambda 生成的類都包含一個對應(yīng)該 lambda 所捕獲的變量的數(shù)據(jù)成員。類似任何普通類的數(shù)據(jù)成員气忠,lambda 的數(shù)據(jù)成員也在 lambda 對象創(chuàng)建時被初始化邻储。

值捕獲

類似參數(shù)傳遞赋咽,變量的捕獲方式也可以是值或引用。到目前為止吨娜,我們的 lambda 采用值捕獲的方式脓匿。與傳值參數(shù)類似,采用值捕獲的前提是變量可以拷貝宦赠。與參數(shù)不同陪毡,被捕獲的變量的值是在 lambda 創(chuàng)建時拷貝,而不是調(diào)用時拷貝:

void  fcn1() {
  size_t  v1 = 42;//局部變量
  // 將 v1 拷貝到名為 f 的可調(diào)用對象
  auto f = [v1] { return  v1; };
  v1 = 0;
  auto j = f2(); // j 為 42; f 保存了我們創(chuàng)建它時 v1 的拷貝
}

由于被捕獲變量的值是在 lambda 創(chuàng)建時拷貝勾扭,因此隨后對其修改不會影響到 lambda 內(nèi)對應(yīng)的值逾雄。

引用捕獲

我們定義 lambda 時可以采用引用方式捕獲變量。例如:

void  fcn2 () {
  size_t  v1 = 42; // 局部變量
  // 對象 f2 包含 v1 的引用
  auto f2 = [&v1] { return v1; };
  v1 = 0;
  auto 3 = f2(); // j 為 0; f2 保存 v1 的引用际邻,而非拷貝
}

v1 之前的 & 指出 v1 應(yīng)該以引用方式捕獲盈电。一個以引用方式捕獲的變量與其他任何類型的引用的行為類似。當我們在 lambda 函數(shù)體內(nèi)使用此變量時燎斩,實際上使用的是引用所綁定的對象虱歪。在本例中,當 lambda 返回 v1 時栅表,它返回的是 v1 指向的對象的值笋鄙。

引用捕獲與返回引用有著相同的問題和限制。如果我們采用引用方式捕獲一個變量怪瓶,就必須確保被引用的對象在 lambda 執(zhí)行的時候是存在的萧落。lambda 捕獲的都是局部變量,這些變量在函數(shù)結(jié)束后就不復存在了洗贰。如果 lambda 可能在函數(shù)結(jié)束后執(zhí)行找岖,捕獲的引用指向的局部變量已經(jīng)消失。

引用捕獲有時是必要的敛滋。例如许布,我們可能希望 biggies 函數(shù)接受一個 ostream 的引用,用來輸出數(shù)據(jù)绎晃,并接受一個字符作為分隔符:

void biggies(vector<string> &words,
              vector<string>::size_type sz,
              ostream &os = cout, char c = ' ') {
    for_each(words.begin(), words.end(), [&os, c](const string &s) { os << s << c; });
}

我們不能拷貝 ostream 對象蜜唾,因此捕獲 os 的唯一方法就是捕獲其引用(或指向 os 的指針)。

當我們向一個函數(shù)傳遞一個 lambda 時庶艾,就像本例中調(diào)用 for_each 那樣 lambda 會立即執(zhí)行袁余。在此情況下,以引用方式捕獲 os 沒有問題咱揍,因為當 for_each 執(zhí)行時颖榜,biggies 中的變量是存在的。

我們也可以從一個函數(shù)返回 lambda。函數(shù)可以直接返回一個可調(diào)用對象朱转,或者返回一個類對象蟹地,該類含有可調(diào)用對象的數(shù)據(jù)成員。如果函數(shù)返回一個 lambda藤为,則與函數(shù)不能返回一個局部變量的引用類似怪与,此 lambda 也不能包含引用捕獲。

建議:盡量保持 lambda 的變量捕獲簡單化

一個 lambda 捕獲從 lambda 被創(chuàng)建(即缅疟,定義 lambda 的代碼執(zhí)行時 ) 到
lambda 自身執(zhí)行(可能有多次執(zhí)行)這段時間內(nèi)保存的相關(guān)信息分别。確保
lambda 每次執(zhí)行的時候這些信息都有預期的意義,是程序員的責任存淫。

捕獲一個普通變量耘斩,如 int、string 或其他非指針類型桅咆,通忱ㄊ冢可以采用簡單的值
捕獲方式。在此情況下岩饼,只需關(guān)注變量在捕獲時是否有我們所需的值就可以了荚虚。

如果我們捕獲一個指針或迭代器,或采用引用捕獲方式籍茧,就必須確保在
lambda 執(zhí)行時版述,綁定到迭代器,指針或引用的對象仍然存在寞冯。而且渴析,需要保
證對象具有預期的值。在 lambda 從創(chuàng)建到它執(zhí)行的這段時間內(nèi)吮龄,可能有代碼
改變綁定的對象的值俭茧。也就是說,在指針(或引用)被捕獲的時刻漓帚,綁定的對
象的值是我們所期望的母债,但在 lambda 執(zhí)行時,該對象的值可能已經(jīng)完全不同了胰默。

一般來說,我們應(yīng)讀盡量減少捕獲的數(shù)據(jù)量漓踢,來避免潛在的捕獲導致的問題牵署。
而且,如果可能的話喧半,應(yīng)試避免捕獲指針或引用奴迅。

隱式捕獲

除了顯式列出我們希望使用的來自所在函數(shù)的變量之外,還可以讓編譯器根據(jù) lambda 體中的代碼來推斷我們要使用哪些變量。為了指示編譯器推斷捕獲列表取具,應(yīng)在捕獲列表中寫一個 & 或 =脖隶。& 告訴編譯器采用捕獲引用方式,= 則表示采用值捕獲方式暇检。例如产阱,我們可以重寫傳遞給 find_if 的 lambda:

// sz 為隱式捕獲,值捕獲方式
wc = find_if(words.begin(), words.end(),
            [=](const string &s){ return s.size() >= sz;});

如果我們希望對一部分變量釆用值捕獲块仆,對其他變量采用引用捕獲构蹬,可以混合使用隱式捕獲和顯式捕獲:

void biggies(vector<string> &words, vector<string>::size_type sz, 
              ostream &os = cout, char c = ' ') {
  // 其他處理
  // os 隱式捕獲,引用捕獲方式悔据;c 顯式捕獲庄敛,值捕獲方式
  for_each(words.begin(), words.end(), [&, c](const string &s) { os << s << c;});
  // os 顯式捕獲,引用捕獲方式科汗;c 隱式捕獲藻烤,值捕獲方式
  for_each(words.begin(), words.end(), [=, &os](const string &s) { os << s << c;});
}

當我們混合使用隱式捕獲和顯式捕獲時,捕獲列表中的第一個元素必須是一個 & 或 =头滔。此符號指定了默認捕獲方式為引用或值怖亭。

當混合使用隱式捕獲和顯式捕獲時,顯式捕獲的變量必須使用與隱式捕獲不同的方式拙毫。即依许,如果隱式捕獲是引用方式(使用 &),則顯式捕獲命名變量必須采取值方式缀蹄,因此不能在其名字前使用 &峭跳。類似地,如果隱式捕獲采用的是值方式(使用 = )缺前,則顯式捕獲命名變量必須采用引用方式蛀醉,即,在名字前使用 &衅码。

lambda 捕獲列表 說明
[] 空捕獲列表拯刁。lambda 不能使用所在函數(shù)中的變量。一個 lambda 只有捕獲變量后才能使用它們
[names] names是一個逗號分隔的名字列表逝段,這些名字都是 lambda 所在函數(shù)的局部變量垛玻。默認情況下,捕獲列表中的變量都被拷貝奶躯。名字前如果使用了 &帚桩,則采用引用捕獲方式
[&] 隱式捕獲列表,采用引用捕獲方式嘹黔。lambda 體中所使用的來自所在函數(shù)的實體都采用引用方式使用
[=] 隱式捕獲列表账嚎,采用值捕獲方式。lambda 體將拷貝所使用的來自所在函數(shù)的實體的值
[&,identifier_list] identifier_list 是一個逗號分隔的列表,包含 0 個或多個來自所在函數(shù)的變量郭蕉。這些變量都采用值捕獲方式疼邀,而任何隱式捕獲的變量都采用引用方式捕獲。identifier_list 中的名字前面不能使用 &
[=,identifier_list] identifier_list 中的變量都采用引用方式捕獲召锈,而任何隱式捕獲的變量都采用值方式捕獲旁振。identifier_list 中的名字不能包括 this,且這些名字之前必須使用 &

可變 lambda

默認情況下烟勋,對于一個值被拷貝的變量规求,lambda 不會改變其值。如果我們希望能改變一個被捕獲的變量的值卵惦,就必須在參數(shù)列表首加上關(guān)鍵字 mutable阻肿。因此,可變 lambda 能省略參數(shù)列表:

void fcn3() {
  size_t v1 = 42; // 局部變量
  // f 可以改變它所捕獲的變量的值
  auto f = [v1]() mutable { return ++v1; };
  v1 = 0;
  auto j = f();  // j 為 43
}

一個引用捕獲的變量是否(如往常一樣)可以修改依賴于此引用指向的是一個 const 類型還是一個非 const 類型:

void fcn4() {
  size_t v1 = 42; // 局部變量
  // v1 是一個非 const 變量的引用
  // 可以通過 f2 中的引用來改變它
  auto f2 = [&v1]() mutable { return ++v1; };
  v1 = 0;
  auto j = f2();  // j 為 1
}

指定 lambda 返回類型

到目前為止沮尿,我們所編寫的 lambda 都只包含單一的 return 語句丛塌。因此,我們還未遇到必須指定返回類型的情況畜疾。默認情況下赴邻,如果一個 lambda 體包含 return 之外的任何語句,則編譯器假定此 lambda 返回 void啡捶。與其他返回 void的函數(shù)類似姥敛,被推斷返回 void 的 lambda 不能返回值。

下面給出了一個簡單的例子瞎暑,我們可以使用標準庫 transform 算法和一個 lambda 來將一個序列中的每個負數(shù)替換為其絕對值:

transform(vi.begin(), vi.end(), vi.begin(),
            [](int i) { return i < 0 ? -i : i;  });

函數(shù) transform 接受三個迭代器和一個可調(diào)用對象彤敛。前兩個迭代器表示輸入序列,第三個迭代器表示目的位置了赌。算法對輸入序列中每個元素調(diào)用可調(diào)用對象墨榄,并將結(jié)果寫到目的位置。如本例所示勿她,目的位置迭代器與表示輸入序列開始位置的迭代器可以是相同的袄秩。當輸入迭代器和目的迭代器相同時,transform 將輸入序列中每個元素替換為可調(diào)用對象操作該元素得到的結(jié)果逢并。

在本例中之剧,我們傳遞給 transform 一個 lambda,它返回其參數(shù)的絕對值砍聊。 lambda 體是單一的 return 語句背稼,返回一個條件表達式的結(jié)果。我們無須指定返回類型辩恼,因為可以根據(jù)條件運算符的類型推斷出來雇庙。

但是,如果我們將程序改寫為看起來是等價的 if 語句灶伊,就會產(chǎn)生編譯錯誤:

// 錯誤:不能推斷 lambda的返回類型
transform(vi.begin(), vi.end(), vi.begin(),
            [](int i) { if(i < 0) return -i; else return i;  });

編譯器推斷這個版本的 lambda 返回類型為 void疆前,但它返回了一個 int 值。

當我們需要為一個 lambda 定義返回類型時聘萨,必須使用尾置返回類型:

transform(vi.begin(), vi.end(), vi.begin(),
            [](int i) -> int { if(i < 0) return -i; else return i;  });

在此例中竹椒,傳遞給 transform 的第四個參數(shù)是一個 lambda,它的捕獲列表是空的米辐,接受單一 int 參數(shù)胸完,返回一個 int 值。它的函數(shù)體是一個返回其參數(shù)的絕對值的 if 語句翘贮。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末赊窥,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子狸页,更是在濱河造成了極大的恐慌锨能,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件芍耘,死亡現(xiàn)場離奇詭異址遇,居然都是意外死亡,警方通過查閱死者的電腦和手機斋竞,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進店門倔约,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人坝初,你說我怎么就攤上這事浸剩。” “怎么了脖卖?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵乒省,是天一觀的道長。 經(jīng)常有香客問我畦木,道長袖扛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任十籍,我火速辦了婚禮蛆封,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘勾栗。我一直安慰自己惨篱,他們只是感情好,可當我...
    茶點故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布围俘。 她就那樣靜靜地躺著砸讳,像睡著了一般琢融。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上簿寂,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天漾抬,我揣著相機與錄音,去河邊找鬼常遂。 笑死纳令,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的克胳。 我是一名探鬼主播平绩,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼漠另!你這毒婦竟也來了捏雌?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤笆搓,失蹤者是張志新(化名)和其女友劉穎腹忽,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體砚作,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨居荒郊野嶺守林人離奇死亡窘奏,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了葫录。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片着裹。...
    茶點故事閱讀 39,731評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖米同,靈堂內(nèi)的尸體忽然破棺而出骇扇,到底是詐尸還是另有隱情,我是刑警寧澤面粮,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布少孝,位于F島的核電站,受9級特大地震影響熬苍,放射性物質(zhì)發(fā)生泄漏稍走。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一柴底、第九天 我趴在偏房一處隱蔽的房頂上張望婿脸。 院中可真熱鬧,春花似錦柄驻、人聲如沸狐树。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽抑钟。三九已至涯曲,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間在塔,已是汗流浹背掀抹。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留心俗,地道東北人。 一個月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓蓉驹,卻偏偏與公主長得像城榛,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子态兴,可洞房花燭夜當晚...
    茶點故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 這是16年5月份編輯的一份比較雜亂適合自己觀看的學習記錄文檔狠持,今天18年5月份再次想寫文章,發(fā)現(xiàn)簡書還為我保存起的...
    Jenaral閱讀 2,752評論 2 9
  • C++ lambda表達式與函數(shù)對象 lambda表達式是C++11中引入的一項新技術(shù)瞻润,利用lambda表達式可以...
    小白將閱讀 85,244評論 15 118
  • 簡介 概念 Lambda 表達式可以理解為簡潔地表示可傳遞的匿名函數(shù)的一種方式:它沒有名稱喘垂,但它有參數(shù)列表、函數(shù)主...
    劉滌生閱讀 3,201評論 5 18
  • 官網(wǎng) 中文版本 好的網(wǎng)站 Content-type: text/htmlBASH Section: User ...
    不排版閱讀 4,380評論 0 5
  • 根據(jù)算法接受一元謂詞還是二元謂詞绍撞,我們傳遞給算法的謂詞必須嚴格接受一個或兩個參數(shù)正勒。但是,有時我們希望進行的操作需要...
    學習編程好少年閱讀 1,442評論 1 0