C++11 | Lambda表達式
- 2020 年 5 月 30 日
- 筆記
- CppGrammar
什麼是Lambda?
先來直觀的看下Lambda表達式:
vector<int> v = {50, -10, 20, -30};
std::sort(v.begin(), v.end()); // the default sort
// now v should be { -30, -10, 20, 50 }
// sort by absolute value:
std::sort(v.begin(), v.end(), [](int a, int b) { return abs(a) < abs(b); });
// now v should be { -10, 20, -30, 50 }
在上述程式碼段中, sort的第三個參數 [](int a, int b) { return abs(a) < abs(b); } 就被稱作Lambda表達式.
它傳入兩個參數a, b並返回它們絕對值比較的結果.
為什麼要學習Lambda, 它和普通的內聯函數有什麼區別?
(1)Lambda定義和使用可以在同一個地方進行的, 便於查閱、調試程式碼;
(2)Lambda可訪問作用域內的任何變數, 也可以指定某些函數中的局部變數(不可全局)交由Lambda捕獲;
(3)C++11引入lambda的主要目的是,能夠將類似於函數的表達式用作接受函數指針或函數符的函數的參數;
(4)典型的Lambda表達式是測試表達式或比較表達式,可編寫一條返回語句;
(5)Lambda表達式參數不能有默認值;
(6)如果Lambda中忽略返回類型, Lambda也可以自動推斷返回類型.
Lambda表達式的一般形式
[capture-list] (parameter-list) mutable(optional) constexpr(optional)(c++17)
exception attribute -> return type { function body }
// C++20中形式如下:
[ captures ] <tparams>(optional)(c++20) ( params ) specifiers
exception attr -> ret requires(optional)(c++20) { function body }
其中:
capture-list:捕獲列表,不能省略. 捕獲列表總是出現於Lambda表達式的開始處, 是Lambda的引出符. 編譯器可依據[]來推斷該函數是否為Lambda函數. 同時”捕獲列表”能夠捕捉使用該表達式的函數中的局部變數, 將變數在Lambda函數體中使用.
Lambda函數能夠捕獲lambda函數外的具有自動存儲時期的變數。函數體與這些變數的集合起來叫閉包。
捕獲的方式可以是引用, 也可以是複製, 像下面這樣:
[]:默認不捕獲任何變數;
[=]:捕獲外部作用域中所有變數,並拷貝一份在函數體中使用(值捕獲);
[&]:捕獲外部作用域中所有變數,並作為引用在函數體中使用(引用捕獲);
[x]:僅以值捕獲x,其它變數不捕獲;
[&x]:僅以引用捕獲x,其它變數不捕獲;
[=, &x]:捕獲外部作用域中所有變數,默認是值捕獲,但是x是例外,通過引用捕獲;
[&, x]:默認以引用捕獲所有變數,但是x是例外,通過值捕獲;
[this]:通過引用捕獲當前對象(其實是複製指針);
[*this]:通過傳值方式捕獲當前對象.
我們可以忽略參數列表和返回類型, 但必須包含捕獲列表和函數體, 所以一個簡單的Lambda表達式像這樣:
auto f = [] { return 66; };
parameter-list: 參數列表, 和普通函數傳參一樣, 可以省略. 這部分對於Lambda表達式是可選的, 故()也可以省略.
mutable:可選關鍵字,將Lambda表達式標記為mutable後,函數體就可以修改傳值方式捕獲的變數. 需要注意的是, 當我們在參數列表後面註明了「mutable」關鍵字之後,則可以取消其類似於class中const成員函數性質. 若在lambda中使用了mutable修飾符,則「參數列表」是不可省略掉的(即使是參數為空).
例如:

這裡輸出的k仍然是5, 但是去掉mutable之後:

在其中就無法修改k值.
但如果通過傳引用的方式捕獲變數, 就不受mutable限制, 且能夠修改局部變數值:

像這樣, 輸出為7.
constexpr:可選,C++17,可以指定lambda表達式是一個常量函數;
exception:可選,指定lambda表達式可以拋出的異常;
attribute:可選,指定lambda表達式的特性;
return type:可選,返回值類型;和C/C++中的普通函數返回值類型的性質一樣。主要目的是用來追蹤lambda函數(有返回值情況下)的返回類型。可省略掉這一部分用於Lambda自動推導.
function body:函數體。在該函數體中,除了可以使用參數列表中的變數外,還可以使用所有捕獲到的變數(即[capture-list] 中的變數).
對於以上參數, 總結如下:
Lambda表達式前面的[]賦予了Lambda表達式很強大的功能, 就是閉包.
Lambda表達式的大致原理:每當你定義一個lambda表達式後,編譯器會自動生成一個匿名類(這個類當然重載了()運算符), 我們稱為閉包類型(closure type).
class ClosureType {
public:
// ...
ReturnType operator(params) const { body };
}
如果帶上mutable關鍵字, 就像這樣:
class ClosureType {
public:
// ...
ReturnType operator(params) { body };
}
在運行時,這個lambda表達式就會返回一個匿名的閉包實例,其實一個右值。所以,我們上面的lambda表達式的結果就是一個個閉包。閉包的一個強大之處是其可以通過傳值或者引用的方式捕捉其封裝作用域內的變數,前面的方括弧就是用來定義捕捉模式以及變數,我們又將其稱為Lambda捕捉塊.
需要注意的是, Lambda表達式無法進行賦值. 但能夠生成副本.
auto a = [] { cout << "A" << endl; };
auto b = [] { cout << "B" << endl; };
a = b; // 非法,lambda無法賦值
auto c = a; // 合法,生成一個副本
// 原因是Lambda中禁用了賦值運算符, 在class中類似這樣:
ClosureType& operator=(const ClosureType&) = delete;
最後, 一個綜合實例如下:
在STL中, 我們也可以將Lambda表達式傳入容器作為比較依據.
//
// Created by hellcat on 2020.05.27.
//
#include <iostream>
#include <set>
using namespace std;
class Person {
public:
Person(const string& first, const string& last) :
m_firstName(first), m_lastName(last) {}
string getFirstName() const { return m_firstName; }
string getLastName() const { return m_lastName; }
private:
string m_firstName;
string m_lastName;
};
int main() {
auto cmp = [](const Person& p1, const Person& p2) {
return p1.getLastName() < p2.getLastName() ||
(p1.getLastName() == p2.getLastName() &&
p1.getFirstName() < p2.getFirstName());
};
set<Person, decltype(cmp)> coll(cmp);
// 調用的構造函數為
// explicit
// set(const _Compare& __comp,
// const allocator_type& __a = allocator_type())
// : _M_t(__comp, _Key_alloc_type(__a)) { }
// 如果調用set默認構造函數, 則會喚醒cmp構造函數, 而Lambda表達式不存在構造函數
coll.insert(Person("Lin", "YANG"));
coll.insert(Person("Li", "YANG"));
coll.insert(Person("hellcat", "shelby"));
cout<<coll.begin()->getFirstName()<<endl;
}
// 輸出為Li.
在這裡如果使用set默認構造函數, 則會有:



