第13課 lambda表達式
1. lambda的語法形式:[capture](params) opt -> ret {body;};
(1)capture為捕獲列表
①[]、[&]和[=]分別表示不捕獲、按引用捕獲、按值捕獲所有父作用域中內的局部變量。(父作用域指包含lambda表達式的語句塊,如main函數)。
◆lambda函數只能捕獲父作用域中的局部變量,而捕獲非父作用域或static變量都會報錯(不是C++11的標準,其行為可能因編譯器而不同)。(註意全局變量或static變量不能被捕獲。即不能被寫入捕獲列表中,但可在lambda的函數體內直接訪問)
◆默認下無法修改按值捕獲的外部變量
◆在類中如果使用&或=捕獲,會同時默認捕獲this指針。
②[=,&foo]按值捕獲外部作用域中所有變量,並按引用捕獲foo變量。註意,捕獲列表不允許變量重復傳遞,如[=,var],var被按值捕獲了兩次,這是不允許的。
③[bar]按值捕獲bar變量(註意只捕獲bar,其他變量不被捕獲)
④[this]捕獲當前類中的this指針,讓lambda表達式擁有和當前類成員函數同樣訪問權限,可以使用當前類的成員函數和成員變量(註意也可修改non-const成員變量的值
(2)params: lambda 表達式的參數列表。
①如果省略,則類似於無參函數func()。
②參數列表中不能有默認參數。所有的參數必須有參數名。
③不支持可變參數。
(3)opt選項:
①mutable修飾符:默認下,lambda表達式的operator()是const函數,mutable可以取消其常量性,讓body內的代碼可以修改被捕獲的變量,並可以訪問被捕獲對象的non-const函數。在使用該修飾符時,參數列表不可省略(即使參數為空)。
②exception:說明lambda表達式是否拋出異常(noexcept),以及拋出何種異常。如拋出整數類型的異常,可以使用throw(int)。
③attribute用來聲明屬性
(4)ret為返回值類型
①如果被省略了,則由return語句的返回類型確定
②如果沒有return語句,則類似於void func(…)函數。
(5)body:函數體
【編程實驗】lambda表達式初體驗
#include <iostream> using namespace std; int g = 0; class Test { public: void func(int x, int y) { //auto x1 = []{return i;}; //error,沒有捕獲外部non-static變量 auto x2 = []{return g++;}; //ok,g不在lambda的父作用域,不能被捕獲。 //如auto x2 = [&g]{return g++;}。但在body內可見! auto x3 = [=]{return i++, i + x + y;};//ok,按值捕獲所有外部變量,由於&或=捕獲時 //會默認地同時傳入this,所以可改變i的值 auto x4 = [&]{return i + x + y;};//ok,按引用捕獲所有外部變量 auto x5 = [this]{return i++;}; ///ok,捕獲了this指針,可以修改成員的值 //auto x6 = [this, x]{return i + x + y;}; //error,沒捕獲y } private: int i = 0; }; int main() { []{}; //最簡單的lambda表達式 //使用返回值後置語法 auto f1 = [](int a) ->int {return a + 1;}; cout << f1(1) << endl; //輸出2 //省略了返回值類型,由return語法推斷 auto f2 = [](int i){return i + 1;}; //註意:初始化列表不能用於返回值的自動 //推導:如auto x = [](){return {1, 2};}; cout << f2(1) << endl; //輸出2 //參數列表為空時可以省略 auto f3 = []{return 1;}; //等價於 auto f3 = [](){return 1;}; cout << f3 << endl; //等價於 cout << f3() << endl; int a = 0, b = 1; //auto f4 = []{return a;}; //error,沒有捕獲外部變量 auto f5 = [&]{return a++;}; //ok,a=1; //auto f6 = [=]{return a++;}; //error,按值捕獲的 auto f7 =[a, &b]{return a + (b++);};//ok,按值捕獲a,按引用捕獲b++ return 0; }
2. lambda與仿函數
(1)仿函數是編譯器實現lambda表達式的一種方式。在現階段,通常編譯器會把lambda表達式轉化成一個仿函數對象。因此在C++11中,lambda可以視為仿函數的一種等價形式。
(2)註意事項
①lambda表達式按值捕獲了所有外部變量。在捕獲的一瞬間,變量x的值就已經被復制了。如果希望lambda表達式在調用時能即時訪問外部變量,應當使用引用方式捕獲。(如變量y)
②默認情況下,按值捕獲的變量是不可以被修改的(如mx++會報錯),因為operator()是const函數。除非在lambda表達式加關鍵字mutable,此時重載的operator()就不會被加上const。但應註意,由於按值捕獲的變量是外部變量的副本,修改他們並不會真正影響到外部變量。
③lambda表達式在C++11中被稱為“閉包類型(Closure Type)”,可以認為它是一個帶有operator()的類(即仿函數),它的捕獲列表捕獲的任何外部變量最終均會變為閉合類型的成員函數。沒有捕獲變量的lambda表達式可以直接轉換為函數指針,而捕獲變量的lambda表達式則不能轉換為函數指針。
【編程實驗】深入分析lambda表達式
#include <iostream> using namespace std; int main() { //1. 按值和按引用捕獲的比較 int a = 12; auto f_val = [=] {return a + 1;}; //按值捕獲:表達式中a的值是外部變量a的副本, //在捕獲一瞬間己確定下來。 auto f_ref = [&] {return a + 1;}; //按引用捕獲 cout << "f_val: " << f_val() << endl; //13 cout << "f_ref: " << f_ref() << endl; //13 a++; //修改a值,a==13 cout << "f_val: " << f_val() << endl;//13,註意這裏輸出沒變! cout << "f_ref: " << f_ref() << endl;//14 //2. 修改按值捕獲的變量(只能影響lambda表達式,不影響外部變量) //auto f1 = [=]{erturn a++;}; //error,operator()是const函數! auto f1 = [=]()mutable{cout << "++a: " << ++a << endl; return a;}; //必須寫參數列表,即使為空! cout << f1() << endl; //14 cout << a << endl; //13 //3. lambda與函數指針的轉換 using func_t = int (*)(int, int); func_t f2 = [](int a, int b){return a + b;}; //ok,捕獲列表必須為空! cout << f2(10, 20) << endl; //30 //4. mutable關鍵字 int val = 0; //auto f3 = [=](){val = 3;}; //編譯失敗:const函數,不能修改按值捕獲的變量 auto f3 = [=]()mutable{ val = 3;};//ok,加了mutable關鍵字 auto f4 =[&](){val = 3;}; //ok,依然是const函數,但可以修改按引用捕獲的變 //量(不過沒改動引用本身) auto f5 = [=](int v) { v = 3;}; //傳參方式將val傳 f5(val); //傳參方式將val傳入,與普通函數的傳參方式等效! return 0; }
3. 使用lambda表達式簡化代碼
(1)lambda被設計出來的主要目的之一就是簡化仿函數的使用,使得在調用標準庫算法的調用時,可以不必定義函數對象,從而大大簡化標準庫的使用。
(2)lambda是就地封裝的短小功能閉包,將其作為局部函數可以輕松地在函數內重用代碼。使得代碼更簡潔,邏輯更清晰。
【編程實驗】使用lambda表達式,使代碼更簡潔、更靈活
#include <iostream> #include <vector> #include <algorithm> #include <functional> using namespace std; using namespace std::placeholders; const int g_ubound = 10; vector<int> nums={8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19,20}; vector<int> largeNums; //顯示vector元素 void show(vector<int>& vec) { for(auto& elem : vec){ cout << elem << " "; } cout << endl; } //函數指針 inline void LargeNumsFunc(int i) { if(i > g_ubound) { largeNums.push_back(i); } } //仿函數 class LargeNums { private: int ubound; public: LargeNums(int u) : ubound(u){}; void operator() (int i) const { if(i > ubound){ largeNums.push_back(i); } } }; //找出vector中大於ubound的元素 void test(int ubound) { //1. 使用傳統的for //缺點: 需要直接使用全局變量ubound for(auto iter = nums.begin(); iter != nums.end(); ++iter){ if(*iter > ubound) largeNums.push_back(*iter); } show(largeNums); largeNums.clear(); //2. 使用函數指針: //缺點:函數定義在別的地方,代碼閱讀不方便 // inline並非強制,可能導致效率問題,特別是循環次數較多的時候 // LargeNumsFunc是個有狀態的函數,直接使用了全局變量(g_ubound), // 函數的可重用性不高! for_each(nums.begin(), nums.end(), LargeNumsFunc); show(largeNums); largeNums.clear(); //3. 使用仿函數 //優點: 仿函數可以擁有狀態,由於for_each第3個參數的只能傳遞一個可調用對象,而不能 // 傳遞額外的參數很有利,因為可以將ubound作為仿函數的參數傳入。 //缺點: 需要單獨定義一個仿函數類 for_each(nums.begin(), nums.end(), LargeNums(ubound)); show(largeNums); largeNums.clear(); //4. 使用lambda表達式 //優點: 比仿函數書寫上更簡便,函數的功能(找出大於ubound的元素)更清晰 for_each(nums.begin(), nums.end(), [=](int i){ //“=”會捕獲test(int ubound)中的ubound變量 if(i>ubound) largeNums.push_back(i); }); show(largeNums); largeNums.clear(); } int main() { //1. 簡化標準庫的調用(統計(50,73]之間元素的個數) vector<int> v{15, 37, 94, 50, 73, 58, 28, 98}; //組合使用bind auto f1 = bind(logical_and<bool>(), bind(greater<int>(), _1, 50), bind(less_equal<int>(), _1, 73)); cout << count_if(v.begin(), v.end(), f1) << endl; //2 //使用lambda表達式 auto f2 = [](int x)->bool {return (50<x)&&(x<=73);}; cout << count_if(v.begin(), v.end(), f2) << endl; //2 //cout << count_if(v.begin(), v.end(), [](int x)->bool {return (50<x)&&(x<=73);}) << endl; //2. lambda表達式與其他callable object的對比 test(g_ubound); return 0; } /*輸出結果 e:\Study\C++11\13>g++ -std=c++11 test3.cpp e:\Study\C++11\13>a.exe 2 2 11 12 13 14 15 16 17 18 19 20 11 12 13 14 15 16 17 18 19 20 11 12 13 14 15 16 17 18 19 20 11 12 13 14 15 16 17 18 19 20 */
第13課 lambda表達式