目錄
目錄 1
1. 前言 1
2. 示例1 1
3. 示例2 2
4. 示例3 3
5. 示例4 3
6. 示例5 6
7. 匿名類規則 6
8. 參考資料 7
1. 前言
本文代碼測試環境為“GCC-9.1.0”,有關編譯器的安裝請參考《安裝GCC-8.3.0及其依賴》,適用於“GCC-9.1.0”。
本文試圖揭露Lambda背后一面,以方便更好的理解和掌握Lambda。Lambda代碼段實際為一個編譯器生成的類的“operator ()”函數,編譯器會為每一個Lambda函數生成一個匿名的類(在C++中,類和結構體實際一樣,無本質區別,除了默認的訪問控制)。
對Lambda的最簡單理解,是將它看作一個匿名類(或結構體),實際上也確實如此,編譯器把Lambda編譯成了匿名類。
2. 示例1
先看一段幾乎最簡單的Lambda代碼:
// g++ -g -o a1 a1.cpp -std=c++11 #include <stdio.h> int main() { auto f = [] { printf("f\n"); }; // 注意“}”后的“;”必不可少,否則編譯報錯 return 0; } |
如果Lambda表達式(或函數)沒有以“;”結尾,則編譯時將報如下錯誤:
a3.cpp: In function 'int main()': a3.cpp:4:3: error: expected ',' or ';' before 'return' 4 | return 0; | ^~~~~~ |
Lambda之所以神奇,這得益於C++編譯器的工作,上述“f”實際長這樣:
type = struct <lambda()> { } |
一個匿名的類(或結構體),實際上還有一個成員函數“operator () const”。注意這里成員函數是”const”類型,這是默認的。如果需非”const”成員函數,需要加”mutable”修飾,如下所示:
auto f = [n]() mutable { printf("%d\n", n); }; |
上面例子對應的匿名類沒有任何類數據成員,現在來個有類數據成員的代碼:
// g++ -g -o a1 a1.cpp -std=c++11 #include <stdio.h> int main() { int n = 3; auto f = [n] { printf("%d\n", n); }; f(); // 這里實際調用的是匿名類的成員函數“operator ()” return 0; } |
這時,“f”實際長這樣,它是一個含有類數據成員的匿名類,而不再是空無一特的類:
type = struct <lambda()> { int __n; } |
3. 示例2
繼續來個變種:
// g++ -g -o a1 a1.cpp -std=c++11 #include <stdio.h> int main() { int n = 3; auto f = [&n]() mutable { printf("%d\n", n); }; f(); return 0; } |
這時,“f”實際長這樣,一個包含了引用類型的匿名類:
type = struct <lambda()> { int &__n; } |
4. 示例3
繼續變種,“&”的作用讓Lambda函數可使用Lambda所在作用域內所有可見的局部變量(包括Lambda所在類的this),並且是以引用傳遞方式:
// g++ -g -o a1 a1.cpp -std=c++11 #include <stdio.h> int main() { int n = 3; auto f = [&]() mutable { printf("%d\n", n); }; f(); return 0; } |
“f”實際長這樣:
type = struct <lambda()> { int &__n; } |
變稍復雜一點:
// g++ -g -o a1 a1.cpp -std=c++11 #include <stdio.h> int main() { int n = 3; int m = 5; auto f = [&]() mutable { printf("%d\n", n); }; f(); return 0; } |
可以看到,“f”並沒有發生變化:
type = struct <lambda()> { int &__n; } |
5. 示例4
繼續增加復雜度:
// g++ -g -o a1 a1.cpp -std=c++11 #include <stdio.h> int main() { int n = 3; int m = 5; auto f = [&]() mutable { printf("%d,%d\n", n, m); }; f(); return 0; } |
可以看到“f”變了:
type = struct <lambda()> { int &__n; int &__m; } |
從上面不難看出,編譯器只會把Lambda函數用到的變量打包進對應的匿名類。繼續一個稍復雜點的:
// g++ -g -o a1 a1.cpp -std=c++11 #include <stdio.h> struct X { void foo() { printf("foo\n"); } void xoo() { auto f = [&] { foo(); }; f(); } }; int main() { X().xoo(); return 0; } |
這時,“f”實際長這樣:
type = struct X::<lambda()> { X * const __this; // X類型的指針(非對象) } |
如果將“auto f = [&] { foo(); };”中的“&”去掉,則會遇到編譯錯誤,提示“this”沒有被Lambda函數捕獲:
a2.cpp: In lambda function: a2.cpp:5:23: error: 'this' was not captured for this lambda function 5 | auto f = [] { foo(); }; | ^ a2.cpp:5:23: error: cannot call member function 'void X::foo()' without object |
改成下列方式捕獲也是可以的:
// g++ -g -o a1 a1.cpp -std=c++11 #include <stdio.h> struct X { void foo() { printf("foo\n"); } void xoo() { auto f = [this] { foo(); }; f(); } }; int main() { X().xoo(); return 0; } |
如果是C++17,還可以這樣:
// g++ -g -o a1 a1.cpp -std=c++17 #include <stdio.h> struct X { void foo() { printf("foo\n"); } void xoo() { auto f = [*this]() mutable { foo(); }; f(); } }; int main() { X().xoo(); return 0; } |
注意得有“mutable”修飾,不然報如下編譯錯誤:
a2.cpp: In lambda function: a2.cpp:5:30: error: passing 'const X' as 'this' argument discards qualifiers [-fpermissive] 5 | auto f = [*this]() { foo(); }; | ^ a2.cpp:3:8: note: in call to 'void X::foo()' 3 | void foo() { printf("foo\n"); } | ^~~ |
也可以這樣:
// g++ -g -o a1 a1.cpp -std=c++17 #include <stdio.h> struct X { void foo() { printf("foo\n"); } void xoo() { auto f = [&,*this]() mutable { foo(); }; f(); } }; int main() { X().xoo(); return 0; } |
使用“*this”時的“f”樣子如下:
type = struct X::<lambda()> { X __this; // X類型的對象(非指針) } |
6. 示例5
繼續研究,使用C++ RTTI(Run-Time Type Identification,運行時類型識別)設施“typeid”查看Lambda函數:
// g++ -g -o a1 a1.cpp -std=c++11 #include <stdio.h> #include <typeinfo> struct X { void xoo() { auto f = [] { printf("f\n"); }; printf("%s\n", typeid(f).name()); // 注:typeid返回值類型為“std::type_info” } }; int main() { X().xoo(); return 0; } |
運行輸出:
ZN1X3xooEvEUlvE_ |
7. 匿名類規則
編譯器為Lambda生成的匿名類規則(不同標准有區別):
構造函數 拷貝構造函數 |
ClosureType() = delete; |
C++14前 |
ClosureType() = default; |
C++20起, 僅當未指定任何俘獲時 |
|
ClosureType(const ClosureType& ) = default; |
C++14起 |
|
ClosureType(ClosureType&& ) = default; |
C++14起 |
|
拷貝復制函數 |
ClosureType& operator=(const ClosureType&) = delete; |
C++20前 |
ClosureType& operator=(const ClosureType&) = default; ClosureType& operator=(ClosureType&&) = default; |
C++20起, 僅當未指定任何俘獲時 |
|
ClosureType& operator=(const ClosureType&) = delete; |
C++20起,其他情況 |
|
析構函數 |
~ClosureType() = default; |
析構函數是隱式聲明的 |
對於標記為“delete”的函數是不能調用的,如下列代碼中的“f2 = f1;”將觸發編譯錯誤:
int main() { auto f1 = []{}; auto f2 = f1; f2 = f1; return 0; } |
上列代碼在C++11、C++14和C++17均會報錯。不過如規則所示,C++20(含C++2a)上則可以正常編譯:
a3.cpp: In function 'int main()': a3.cpp:4:8: error: use of deleted function 'main()::<lambda()>& main()::<lambda()>::operator=(const main()::<lambda()>&)' 4 | f2 = f1; | ^~ a3.cpp:2:14: note: a lambda closure type has a deleted copy assignment operator 2 | auto f1 = []{}; | ^ |
希望通過本文,對理解Lambda有所幫助。
8. 參考資料
1) https://zh.cppreference.com/w/cpp/language/lambda
2) https://docs.microsoft.com/en-us/cpp/cpp/lambda-expressions-in-cpp?view=vs-2019
3) https://en.cppreference.com/w/cpp/language/lambda
4) https://stackoverflow.com/questions/7627098/what-is-a-lambda-expression-in-c11
5) https://www.cprogramming.com/c++11/c++11-lambda-closures.html