編譯器如何實(shí)現(xiàn)lambda表達(dá)式?
本文轉(zhuǎn)載自微信公眾號「程序喵大人」,作者程序喵大人。轉(zhuǎn)載本文請聯(lián)系程序喵大人公眾號。
lambda表達(dá)式在C++11中引入,用lambda表達(dá)式表示匿名函數(shù)非常方便,語法很簡單,而且可以使代碼更緊湊,更易于閱讀。
lambda表達(dá)式更適合定義小點(diǎn)的回調(diào)內(nèi)聯(lián)去傳遞給其他函數(shù),而不是在其他地方定義個(gè)完整的函數(shù)對象,并在其重載函數(shù)調(diào)用運(yùn)算符中實(shí)現(xiàn)回調(diào)邏輯。所有的邏輯都在一個(gè)位置上,容易理解和維護(hù),lambda表達(dá)式可以接收參數(shù),可返回值,可模板化,可通過值或引用的方式訪問外面的變量,相當(dāng)?shù)撵`活。
關(guān)于lambda表達(dá)式的使用,我之前介紹過,可以看這篇文章搞定c++11新特性std::function和lambda表達(dá)式,這里一筆帶過:
- auto lambda { []{ cout << "Hello \n"; } };
- lambda();
那這個(gè)lambda表達(dá)式是如何實(shí)現(xiàn)的呢?
編譯器會將lambda表達(dá)式自動轉(zhuǎn)換為函數(shù)對象,編譯器會為此生成個(gè)唯一的命名。上面的示例會自動的轉(zhuǎn)換成下面這樣的函數(shù)對象,注意函數(shù)調(diào)用運(yùn)算符是個(gè)const方法,返回類型是auto,這方便編譯器根據(jù)方法體自動推導(dǎo)出返回類型。
- class CompilerGeneratedName {
- public:
- auto operator()() const { cout << "Hello \n"; }
- };
編譯器生成的lambda閉包名字會是一些奇怪的名子,例如__Lambda_21Za等,我們沒法知道這個(gè)名字,我們也不需要知道這個(gè)名字。
lambda表達(dá)式可以接收參數(shù),參數(shù)在圓括號之間指定,就像普通函數(shù)一樣,下面是例子:
- auto lambda {
- [](int value){ cout << "The value is " << value << endl; } };
- lambda(42);
如果lambda表達(dá)式不接收任何參數(shù),可以指定空括號或者直接省略括號。
編譯器會將上面的lambda表達(dá)式自動轉(zhuǎn)換為下面這樣:
- class CompilerGeneratedName {
- public:
- auto operator()(int value) const {
- cout << "The value is " << value << endl; }
- };
lambda表達(dá)式可以返回值,返回類型在箭頭后面指定,稱為尾返回類型,看代碼:
- auto lambda { [](int a, int b) -> { return a + b; } };
- int sum = lambda(11, 22);
編譯器轉(zhuǎn)成這樣:
- class CompilerGeneratedName {
- public:
- auto operator()(int a, int b) const { return a + b; }
- };
那能捕獲變量的lambda表達(dá)式是怎么實(shí)現(xiàn)的呢?
比如下面的lambda表達(dá)式:
- double data { 1.234 };
- auto lambda { [data]{ cout << "Data = " << data << endl; } }
捕獲的變量會變?yōu)閘ambda閉包的數(shù)據(jù)成員,值捕獲的變量被拷貝到仿函數(shù)的數(shù)據(jù)成員中,編譯器的行為是這樣:
- class CompilerGeneratedName
- {
- public:
- CompilerGeneratedName(const double& d) : data { d } {}
- auto operator()() const { cout << "Data = " << data << endl; }
- private:
- double data;
- };
還有泛型lambda表達(dá)式:
- auto areEqual { [](const auto& value1, const auto& value2) {
- return value1 == value2; } };
- vector values1 { 2, 5, 6, 9, 10, 1, 1 };
- vector values2 { 4, 4, 2, 9, 0, 3, 1 };
- findMatches(values1, values2, areEqual, printMatch);
編譯器會轉(zhuǎn)換成這樣:
- class CompilerGeneratedName {
- public:
- template <typename T1, typename T2>
- auto operator()(const T1& value1, const T2& value2) const
- { return value1 == value2; }
- };
如果findMatches()函數(shù)中的參數(shù)是其他類型,那么areEqual泛型表達(dá)式不需要任何更改就可以直接繼續(xù)使用。
聊完了編譯器怎么實(shí)現(xiàn)的lambda表達(dá)式,下面介紹下lambda表達(dá)式的捕獲方式。
捕獲方式
有兩種方法從閉包作用域捕獲所有變量,稱為默認(rèn)捕獲:
- [=] 值捕獲所有變量
- [&]引用捕獲所有變量
- 注意:
- 使用引用方式捕獲變量時(shí),必須確保引用在lambda表達(dá)式執(zhí)行期間是合法的。
- 當(dāng)使用默認(rèn)捕獲時(shí),通過值(=)或引用(&),只有那些在lambda 表達(dá)式中真正使用的變量才會被捕獲,未使用的變量不會被捕獲。
- 不建議使用默認(rèn)捕獲,即使默認(rèn)捕獲只捕獲那些在lambda 表達(dá)式主體中真正使用的變量,通過使用=默認(rèn)捕獲,可能會意外的導(dǎo)致高代價(jià)的拷貝,通過使用&默認(rèn)捕獲,可能意外的在閉包作用域中修改變量,建議明確指定想要捕獲哪些變量以及捕獲方式。
再注意:全局變量總是通過引用捕獲,例如在下面的代碼中,默認(rèn)捕獲用于按值捕獲所有內(nèi)容,然而全局變量global其實(shí)是通過引用捕獲的,在執(zhí)行l(wèi)ambda 后它的值被更改。
- int global { 42 };
- int main() {
- auto lambda { [=] { global = 2; } };
- lambda();
- // 這里global是2!
- }
不允許像下面這樣顯式捕獲全局變量,這樣編譯會失?。?/p>
- auto lambda { [global] { global = 2; } }; // error
所以,建議不要使用全局變量。
對于不捕獲任何內(nèi)容的lambda表達(dá)式,編譯器自動提供轉(zhuǎn)換運(yùn)算符,將lambda 表達(dá)式轉(zhuǎn)換為函數(shù)指針。這樣的lambda表達(dá)式可作為參數(shù)傳遞給其他函數(shù)。
在C++20中關(guān)于lambda表達(dá)式也做了一些更新,可以模板化lambda表達(dá)式,也可以默認(rèn)構(gòu)造、拷貝和賦值lambda表達(dá)式,像下面這樣:
- auto lambda { [](int a, int b) { return a + b; } };
- decltype(lambda) lambda2; // 默認(rèn)構(gòu)造
- auto copy { lambda }; // 拷貝構(gòu)造
- copy = lambda2; // 拷貝賦值
這不是本文的主題,就不過多介紹了。