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