我們可以向一個算法傳遞任何類別的可調(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 語句翘贮。