第17課 lambda表達式

一. lambda表達式ios

(一)語法定義[capture](paramters) mutable ->returnType{statement}算法

  1.[capture]:捕獲列表編程

  (1)lambda函數只能捕獲父做用域中的局部變量或形參。而捕獲非父做用域或靜態變量則會出錯。(這裏的父做用域指的是包含lambda函數的語句塊,如main函數做用域)閉包

    ①[]:表示不捕獲;[=]和[&]:分別表示按值和按引用捕獲全部父做域變量(包括this);less

    ②[var]、[&var]分別表示按值和按引用捕獲var。注意,默認是沒法修改按值捕獲的變量的值(由於lambda表達式的operator()默認爲const)。ide

    ③[=,&foo]:表示按引用捕獲foo變量,按值捕獲父做用域中全部其它變量。函數

    ④[this]:捕獲當前類中的this指針,讓lambda表達式擁有和當前類成員函數一樣訪問權限。捕獲this的目的是能夠在lambda中會使用當前類的成員函數和成員變量。性能

  (2)注意事項:this

    ①捕獲列表不容許變量重複傳遞。如[=,a]、[&,&this]其中的a和this都被重複傳遞。spa

    ②lambda表達式的按值捕獲,是在聲明lambda表達式的一瞬間就被複制了若是但願lambda表達式在調用時能即時的訪問外部變量,應該使用按引用捕獲

    ③默認狀況下,按值捕獲的變量是不能夠被修改的,由於lambda表達式的operator()是個const函數。

    ④lambda不能捕獲非父做用域變量或static變量。即它們不能被進入捕獲列表中,但可在lambda的函數體內直接訪問。

    2.(parameters):參數列表。

  (1)與普通函數的參數列表一致。若是不須要參數傳遞,則能夠連同括號()一塊兒省略。

  (2)參數列表不支持默認值,也不支持可變參數。全部的參數必須有參數名。

  (3)C++14中,參數類型能夠聲明爲auto類型

    3.mutable默認下,lambda函數老是一個const函數,mutable能夠取消其常量性。在使用該修飾符時,參數列表不可省略(即便參數爲空)

    4.returnType返回值類型。用追蹤返回類型形式聲明函數的返回類型。在返回類型明確的狀況下,也能夠省略該部分,讓編譯器對返回類型進行自動推導。若是沒有return語句則返回void。

    5.{statement}函數體。內容與普通函數同樣,除了可使用參數以外,還可使用全部捕獲的變量。

(二)lambda表達式與仿函數

 

    1. 仿函數是編譯器實現lambda表達式的一種方式。在現階段,一般編譯器都會把lambda表達式轉化成爲一個仿函數對象。所以,在C++11中,lambda能夠視爲仿函數的一種等價形式或叫「語法糖」。

    2. 二者雖然在語法層面上不一樣,但卻有着相同的內涵爲——均可以捕捉一些變量做爲初始狀態並接受參數進行運算。

    3. lambda表達式在C++11中被稱爲閉包類型(Closure Type),能夠認爲是個仿函數,帶有const屬性的operator()它的捕獲列表捕獲的任何外部變量最終均會變爲仿函數的成員變量。由閉包類型定義的對象稱爲「閉包」(是個右值)。

  4. 沒有捕獲變量的lambda表達式能夠直接轉換爲函數指針,而捕獲變量的lambda表達式則不能轉換爲函數指針

【編程實驗】lambda初體驗

#include <iostream>

using namespace std;

int gVal = 0;

//捕獲this指針
class Test
{
private: 
    int i = 0;
public:
    void func(int x, int y)
    {
        int a = 0;
        //auto lamb1 = [] {return i; }; //error,無捕獲列表。
        //auto lamb2 = [&i] {return i; }; //error, 不能捕獲父做用域(func域)之外的變量(i)
        auto lamb3 = [=] {return i; }; //ok,按值捕獲(含this指針),所以能夠訪問類中的成員變量(i)。
        auto lamb4 = [&] {return i + x + a; }; //ok,按引用捕獲(含this指針),可使用類中的成員變量(i)
                                               //同時,也捕獲到形參x和局部變量a。
        auto lamb5 = [this] {return i; };  //ok,直接捕獲this指針。

        auto lamb6 = [] {return gVal++; }; //ok,可使用直接使用全局變量,無須也不能捕獲它。
    }
};

int main()
{
    int a = 3;
    int b = 4;

    //1. lambda表達式初體驗
    auto lamb1 = [] {};   //最簡單的lambda表達式

    auto lamb2 = [=] {return a + b; };//省略參數列表和返回類型
    cout << lamb2() << endl; //7

    auto lamb3 = [&](int c) {b = a + c; };//省略返回類型,爲void。
    //cout << lamb3(5) << endl; //error,返回void

    auto lamb4 = [] {return 1; }; //省略參數列表
    cout << lamb4() << endl;

    auto lamb5 = [=, &b](int c)->int {return b += a + c; }; //各部分完整的lambda表達式
    cout << "lamb5(2) = "<<lamb5(2) << ", b = " << b<< endl; //9, 9

    //2. lambda表達式的常量性及mutable關鍵字
    a = 1;
    //auto f1 = [] {return a++; };  //error,沒有捕獲外部變量
    //auto f2 = [=]() { a = 1;};    //error,const函數不能修改按值捕獲的變量
    auto f2 = [=]() mutable { a = 2; }; //ok,被mutable修飾
    auto f3 = [&a]() { a = 3; };       //ok,按引用傳遞。const函數時影響引用自己,表示其不可修改
                                       //但其引用的內容不受const影響,仍可修改。
    //3. 捕獲的時間點
    int x = 10;
    auto lambByVal = [x] {return x + 1; }; //按值捕獲:聲明時,x被複制一下
    auto lambByRef = [&x] {return x + 1; };//按引用捕獲:x的值是隨外部x的變化而變化。
    cout << "lambByVal() = "<< lambByVal() << endl; //11
    cout << "lambByRef() = "<< lambByRef() << endl; //11
    
    ++x;

    cout << "lambByVal() = " << lambByVal() << endl; //11
    cout << "lambByRef() = " << lambByRef() << endl; //12

    //4. lambda表達式轉換爲函數指針
    using FuncX = int(*)(int);
    using FuncXY = int(*)(int, int);

    int k = 1;
    auto lambN = [](int x, int y) {return x + y; };       //無捕獲列表
    auto lambK = [&k](int x, int y) {return x + y + k; }; //有捕獲列表

    FuncXY funcXY;
    funcXY = lambN; //ok,無捕獲列表的lambda可轉化爲函數指針
    //lambN = funcXY; //error,不能將函數指針轉爲lambda
    //funcXY = lambK; //error,有捕獲列表的lambda不能轉爲函數指針

    //5. 捕獲this指針(見Test類)

    return 0;
}
初識lambda

(三)泛型lambda表達式

    1. 概述:

 

  (1)泛型lambda的格式形如 [](auto x, auto y){}; 或 [](auto&& x, auto&& y){} 等。

  (2)因爲auto&&類型的形參沒有可用的T類型,泛型lambda採用forward+decltype來轉發param

  (3)當param被左值實參初始化時,param被推導爲左值引用,即decltype(param)爲左值引用類型。同理,當param被右值初始化時,即decltype(param)爲右值引用類型。

  (4)decltype(param)做爲模板形參傳入std::forward時,會發生引用摺疊,從而能正確根據實參的左/右值特性進行轉發。

  2. 泛型lambda的優點

  (1)使用auto做爲lambda函數的參數類型修飾符,增長泛型編程能力

  (2)泛型lambda容許帶auto參數的lambda函數可以轉化爲函數指針

【編程實驗】泛型lambda

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

int main()
{
    //1. 泛型局部函數
    auto f1 = [](auto x, auto y) {return x + y; };
    cout <<"f1(1, 3) = " << f1(1, 3) << endl;
    cout <<"f1(4.5, 6.3) = " << f1(4.5, 6.3) << endl;
    cout << "f1(\"abc\", \"def\") = " << f1(string{ "abc" }, "def") << endl;

    //2. 泛型回調函數
    auto f2 = [](auto x) {cout << x << ","; };
    vector<int> v1{1, 2, 3};
    vector<string> v2{ "a","b","c" };
    for_each(v1.begin(), v1.end(), f2); cout << endl;
    for_each(v2.begin(), v2.end(), f2); cout << endl;

    //3. 泛型lambda與函數指針的轉換
    auto f3 = [](auto x){ return x; };
    using Func = int(*)(int);
    Func pf = f3;
    cout << pf(5) << endl;  //5

    cout << "----------------------------------"<< endl;
    //4. 利用泛型lambda模擬實現tuple。
    //(1)make_tuple接受可變參數包,返回一個tuple對象(lambda)。
    //   如,auto tp = make_tuple(1, '2', "3");即tp = [](auto access){return access(1, '2', "3")};
    //   可見參數包己被展開,並保存在tp中,可隨時供訪問器使用。
    //(2)該tuple的使用方式是:tuple(access),向其傳入某種功能的訪問器(如打印,求長度函數)
    //   這種經過tuple+access的方式能夠達到訪問tuple中元素的目的。注意,access爲可接受可變參數包的
    //   可調用對象。
    auto make_tuple = [](auto ...xs) {
        return [=](auto access) { return access(xs...); };
    };

    //4.1 求tuple的長度
    auto length = [](auto xs) { //length(tp),傳入tuple
        return xs([](auto ...z) { return sizeof...(z); }); //經過tp(access)向其傳入計算元素個數的訪問器。
    };

    //4.2 fmap的功能: 將一個tuple(tpSrc)經過func函數映射成另外一個新的tuple。如經過對元素*2,
    //將tuple(1,2,3,4)映射成tuple(2,4,6,8)
    auto fmap = [=](auto func) {  //捕獲make_tuple
        return [=](auto tpSrc) {  //捕獲func和make_tuple
            return tpSrc([=](auto... xs) { return make_tuple(func(xs)...); }); //tuple+訪問器方式
        };
    };

    auto tp = make_tuple(1, '2', "3");
    std::cout << length(tp) << std::endl; // 3
    int len = tp([](auto...z) { return sizeof...(z); }); //經過向tuple傳入訪問器的方式來使用tuple。
                                                      //注意訪問器必須可接受可變參數包。
    std::cout << len << std::endl;

    auto twice = [](auto i) { return 2 * i; };  //映射函數
    auto print = [](auto i) { std::cout << i << " "; return i; };
    auto tp1 = make_tuple(1, 2, 3, 4);
    auto tp2 = fmap(twice)(tp1); //將tp1經過twice函數映射成tp2
    auto tp3 = fmap(print)(tp2); //將tp2經過print函數映射成tp3,並經過print將tp2元素打印出來。
                                 //make_tuple(func(xs)...) 等價於make_tuple(func(xs1),func(xs2),...)
                                 //因參數按從右向左依次傳入,因此最終打印結果爲8,6,4,1

    return 0;
}
/*輸出結果
f1(1, 3) = 4
f1(4.5, 6.3) = 10.8
f1("abc", "def") = abcdef
1,2,3,
a,b,c,
5
----------------------------------
3
3
8 6 4 2
*/

二. lambda表達式的優點

(一)使代碼更加簡化、邏輯更清晰。

(二)使程序更靈活,在須要的時間和地點實現閉包

(三)簡化仿函數的使用,使得STL算法的使用更加容易

【編程實驗】lambda的優點

#include <iostream>
#include<algorithm>
#include <functional>

using namespace std;
using namespace std::placeholders;

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 print(vector<int>& vec)
{
    for (auto& elem : vec) {
        cout << elem << " ";
    }

    cout << endl;
}

//函數
inline void LargNumsFunc(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);
        }
    }
};
void test(int ubound)
{
    //1. 使用傳統的for(缺點:須要直接使用全局變量g_ubound)
    for (auto iter = nums.begin(); iter != nums.end(); ++iter) {
        if (*iter > ubound) {
            largeNums.push_back(*iter);
        }
    }
    print(largeNums);

    largeNums.clear();
    //2.使用函數指針
    //缺點:函數定義在別的地方,代碼閱讀不方便。inline非強制性的,內聯不必定成功。可
    //      能致使性能問題。且LargeNumFunc因爲使用了全局變量,是個有狀態函數,函數重用性不高。
    for_each(nums.begin(), nums.end(), LargNumsFunc);
    print(largeNums);

    largeNums.clear();
    //3. 使用仿函數
    //優勢:仿函數能夠擁有狀態,因爲for_each第3個參數只能傳遞一個可調用對象而不能傳遞額外的參數。
    //      所以,利用仿函數就能夠克服這一不足。
    //缺點:須要單獨定義一個仿函數類。
    for_each(nums.begin(), nums.end(), LargeNums(g_ubound));
    print(largeNums);

    largeNums.clear();
    //4. 使用lambda表達式
    //優勢:比仿函數書寫上更簡潔,代碼的功能更清晰。
    for_each(nums.begin(), nums.end(), [ubound](int i) 
    {
            if (i > ubound) {
                largeNums.push_back(i);
            }
    });
    print(largeNums);
}
int main()
{
    //1. lambda可簡化標準庫的調用(統計(50,73]之間的元素個數)
    vector<int> v{ 15, 37, 94, 50, 73, 58, 28, 98 };
    //1.1 組合使用bind
    auto f1 = std::bind(std::logical_and<bool>(),
                        std::bind(std::greater<int>(), _1, 50),
                        std::bind(std::less_equal<int>(), _1, 73)
                        );
    cout << count_if(v.begin(), v.end(), f1) << endl; //2

    //1.2 使用lambda表達式
    auto f2 = [](int x) {return (50<x) && (x<=73); };
    cout << count_if(v.begin(), v.end(), f2) << endl; //2
    int cnt = count_if(v.begin(), v.end(), [](int x)
              {
                 return  (50 < x) && (x <= 73);
              });
    cout << cnt << endl; //2

    //2. lambda與其餘可調用對象使用上的比較
    test(g_ubound);

    return 0;
}
/*輸出結果
2
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
*/
相關文章
相關標籤/搜索