C++11的閉包(lambda、function、bind)


c++11開始支持閉包,閉包:與函數A調用函數B相比較,閉包中函數A調用函數B,可以不通過函數A給函數B傳遞函數參數,而使函數B可以訪問函數A的上下文環境才可見(函數A可直接訪問到)的變量;比如:

    函數B(void) {

        ......

    }

    函數A {

        int a = 10;

        B();    //普通調用函數B

    }

    函數B無法訪問a;但如果是按閉包的方式,則可以訪問變量a:

    函數A() {

        int a = 10;

        auto closure_B = [a]() {

            ......

        }

    }

    所以閉包的優缺點很清晰,都是同一個:可以不通過傳參獲取調用者的上下文環境;

   

    下面開始介紹c++11的閉包所涉及的內容:

    1、lambda

    1.1、lambda的定義

    1.2、lambda的應用

    1.3、lambda的使用注意事項

    2、function

    2.1、通過lambda的function

    2.2、通過函數的function



    1、lambda

    1.1、lambda的定義:

        匿名表達式的結構:auto lambda = [捕獲列表] (參數列表) (修飾符) {匿名函數體};

        關於捕獲列表:捕獲調用者上下文環境的需要訪問的變量,可按值捕獲或按引用捕獲,其規則如下:

        []:什么也不捕獲

        [=]:捕獲所有一切變量,都按值捕獲

        [&]:捕獲所有一切變量,都按引用捕獲

        [=, &a]:捕獲所有一切變量,除了a按引用捕獲,其余都按值捕獲

        [&, =a]:捕獲所有一切變量,除了a按值捕獲,其余都按引用捕獲

        [a]:只按值捕獲a

        [&a]:只按引用捕獲a

        [a, &b]:按值捕獲a,按引用捕獲b

        關於參數列表:

        和函數調用的參數列表含義完全一樣;

        關於修飾符:

        mutable:必須注意加入mutable可以修改的是:在lambda匿名函數體里邊,按值捕獲到的變量,實質上是調用者函數中變量的只讀拷貝(read-only),加入了mutable后,匿名函數體內部可以修改這個拷貝的值,也就是說調用者函數中該變量的值依然不會被改變;

關於匿名函數體:

        和函數沒有區別;

    1.2、lambda的應用:

        a. 最簡單的使用:

 

 1 TEST (test1, lambda_1) {
 2 //capture list is empty, all the args is value, so variable b will not change.
 3     int a = 1, b = 2;
 4     auto lambda = [](int a, int b) {
 5         b = a + a + a;
 6         return a + b;
 7     };
 8  
 9     std::cout << lambda(a, b) << std::endl;
10     std::cout << b << std::endl;
11 }

這里捕獲列表為空,完全相當於調用普通函數

        b. 捕獲列表非空:

 1 TEST (test1, lambda_2) {
 2 //capture list is not empty, x is value, y is reference, and arg z is reference
 3 //so y will change to 1, z will change to 1
 4 //x is value, so x is read-only in lambda, should not modify x in lambda
 5     int x = 1, y = 2;
 6     auto lambda = [x, &y](int &z) {
 7         z = x;
 8         y = x;
 9         //x = y;
10         return y;
11     };
12  
13     int z = 0;
14     std::cout << lambda(z) << std::endl;
15     std::cout << y << std::endl;
16     std::cout << z << std::endl;
17 }

捕獲列表非空,變量x傳值,變量y傳引用,並且傳一個參數z;

        c. 捕獲全部變量 & 使用mutable修飾符:

 1 TEST (test1, lambda_3) {
 2 //'=' in capture list means that all the variables will as value in lambda, so should not modify a, a is read-only in lambda
 3     int a = 1;
 4     auto lambda_val = [=]() {
 5         //a = 10;
 6         return a + 1;
 7     };
 8     std::cout << lambda_val() << std::endl;
 9     std::cout << a << std::endl;
10  
11 //'&' '=' in capture list means that all the variables will as reference in lambda, so could modify a
12     auto lambda_ref = [&]() {
13         a = a + 1;
14         return a + 1;
15     };
16     std::cout << lambda_ref() << std::endl;
17     std::cout << a << std::endl;
18  
19 //though '=' in capture list means all the variables as value in lambda_val_mutable, but lambda_val_mutable is mutable, so also could modify a
20     auto lambda_val_mutable = [=]() mutable {
21         a = 10;
22         return a + 1;
23     };
24     std::cout << lambda_val_mutable() << std::endl;
25     std::cout << a << std::endl;
26 }

 

使用[=]按值、[&]按引用捕獲一切變量;使用mutable修飾符,在匿名函數體內部修改按值傳入的變量,注意在調用者函數不會被修改生效;

        d. lambda的定義時初始化:

 

 1 TEST (test1, lambda_4) {
 2 //all capture variables will value in lambda, so a will not change, return a + 1 = 2
 3     int a = 1;
 4  
 5     a = 3;
 6     auto lambda_val = [=]() {
 7         //a = b;
 8         return a + 1;
 9     };
10  
11     int b = 10;
12     std::cout << lambda_val() << std::endl;
13     std::cout << a << std::endl;
14  
15 //a is change to 10, but for lambda_val, a is inited(read-only), will not change to 10, forever is 1
16     a = 10;
17     std::cout << lambda_val() << std::endl;
18     std::cout << a << std::endl;
19 }

 

lambda表達式定義時,調用者上下文環境中的變量有哪些、值都是什么,是"一次性"初始化的,也就是說即便后面值被修改,但lambda是無法獲知的,后面再創建的新的變量,lambda是無法訪問的;

        e. 部分變量傳引用或者傳值:

 1 TEST (test1, lambda_5) {
 2 //exclude b is reference in lambda, all the other variables in capture list will be value in lambda
 3     int a = 1, b = 2, c = 3, d = 4, e = 5;
 4     auto lambda = [=, &b](int f) {
 5         b = a + c + d + e;
 6         return a + b + c + d + e;
 7     };
 8  
 9     std::cout << lambda(6) << std::endl;
10     std::cout << b << std::endl;
11 }

 

 f. 類成員函數使用lambda:

 1 TEST (test1, lambda_6) {
 2 //in a class-function, lambda's capture list is this point, so could access and modify the class non-const variable
 3     class cls {
 4         int a;
 5         int b;
 6         int c;
 7         const int d;
 8     public:
 9         cls():a(1), b(2), c(3), d(5) {}
10         ~cls(){}
11         void testlambda() {
12             auto lambda = [this]() {
13                 a = 10;
14                 //d = 1;
15                 return a + b + c + d;
16             };
17  
18             std::cout << a << std::endl;
19             std::cout << lambda() << std::endl;
20             std::cout << a << std::endl;
21         }
22     };
23  
24     cls c;
25     c.testlambda();
26 }

 

 this指針進入捕獲列表,匿名函數體內部即可調用類成員變量;

 g. c++11風格的lambda的使用:

 

TEST (test1, lambda_7) {
    std::vector<int> v = {3,5,2,1,7,8,6,4,10,9};
 
    std::sort(begin(v), end(v), [](int i, int j) {
        return i > j;
    });
 
    std::for_each(begin(v), end(v), [](int i) {
        std::cout << i << " ";
    });
    std::cout << std::endl;
 
    int total = 0;
    for_each (begin(v), end(v), [&](int i) {
        total += i;
    });
    std::cout << total << std::endl;
 
    int base = 2;
    for_each (begin(v), end(v), [=](int i) mutable {
        total += (i * base);
        std::cout << total << "," << i << "," << base << std::endl;
    });
    std::cout << total << std::endl;
 
    for_each (begin(v), end(v), [&](int i) mutable {
        total += (i * base);
        std::cout << total << "," << i << "," << base << std::endl;
    });
    std::cout << total << std::endl;
}

 

像python一樣的使用;

    1.3、lambda使用的注意事項:

        lambda常常作為線程執行函數使用,這時尤其要注意調用者上下文環境的變量(及其指向的內存空間)的生命周期,是否能夠和以lambda作為線程執行函數的線程的生命周期一樣長,如下面的兩個例子:

        會導致出問題的:       

TEST (test3, closure_wrong) {
    int *a = new int(10);
    std::thread th = std::thread([&a] () {
        while (1) {
            std::cout << "a: " << a << ", *a: " << *a << std::endl;
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    });
 
    std::this_thread::sleep_for(std::chrono::seconds(3));
    delete a;
    a = nullptr;
    std::this_thread::sleep_for(std::chrono::seconds(30));//closure will collapse...
}
        動態變量a申請了4字節的動態空間,作為線程執行函數的匿名函數使用了變量a指向的動態空間;然后a在調用者函數內部釋放了動態空間,但匿名函數依然訪問該動態空間,此時導致發生程序崩潰;

        不會導致出問題的:

TEST (test3, closure_right) {
    int *a = new int(10);
    std::thread th = std::thread([a] () {
        while (1) {
            std::cout << "a: " << a << std::endl;
            std::this_thread::sleep_for(std::chrono::seconds(1));
        }
    });
 
    th.detach();
    std::this_thread::sleep_for(std::chrono::seconds(3));
    delete a;
    a = nullptr;
    std::this_thread::sleep_for(std::chrono::seconds(30));//closure will not collapse
}
        上面的程序不會出現問題,因為匿名函數內部沒有訪問變量a指向的動態空間。但依然是不推薦的方式。應確保匿名函數訪問的外部捕獲變量,不會在線程生命周期內失效;


    2、function:

        掌握了lambda,function就非常容易掌握;c++11定義一個函數std::function,可以綁定一個匿名函數表達式lambda,也可以綁定一個普通函數(包括類成員函數);

    2.1、綁定lambda匿名函數:

TEST (test2, function_lambda) {
    int a = 1, b = 2;
    std::function<int (const int &)> func1 = std::function<int (const int &)>([a, &b] (const int &i) {
        b = a + i;
        return a + b;
    });
 
    int c = 10;
    std::cout << func1(std::cref(c)) << std::endl;
    std::cout << a << std::endl;
    std::cout << b << std::endl;
 
    std::function<int (const int &, int &)> func2 = std::function<int (const int &, int &)>([a, &b] (const int &i, int &j) {
        int k = i + j;
        b = a + k;
        j = a + b;
        return a + b + j;
    });
 
    int d = 10, e = 20;
    std::cout << func2(d, e) << std::endl;
    std::cout << b << std::endl;
    std::cout << e << std::endl;
 
    class cls {
        int a;
        int b;
        int c;
 
    public:
        cls(int x, int y, int z):a(x), b(y), c(z){}
        ~cls(){}
 
        double CalcAverage() {
            auto calcer = std::function<double ()>([this] {
                return (a + b + c)/3.0;
            });
 
            return calcer();
        }
    };
 
    std::vector<int> v;
    std::random_device rd;
    for (auto idx: common::Range(0, 3)) {
        int i = rd() % 10;
        v.push_back(i);
        std::cout << "input " << i << std::endl;
    }
    cls cl(v[0], v[1], v[2]);
    double average = cl.CalcAverage();
    std::cout << std::fixed << std::setprecision(6) << average << std::endl;
}
        函數func1、func2就是綁定lambda的匿名函數;而cls的類成員函數CalcAverage,就是編寫了一個匿名函數calcer再調用的閉包方式;

    2.2、綁定普通函數(包括類成員函數):

int func1 (const int &i, const int j, int &k) {
    k = i - j;
    return i + j;
}
int func2 (int &i, double j, const std::string k) {
    return i + (int)j;
}
TEST (test2, function_simple_and_bind) {
    int i = 0, j = 1, k = 2;
    auto f1 = std::function<int (const int &, const int, int &)>(func1);
    std::cout << f1(std::cref(i), j, std::ref(k)) << std::endl;
    std::cout << k << std::endl;
    
//if use bind and adjust the arg order, such as: arg_3 is actively arg_1, arg_1 is actively arg_2, arg_2 is actively arg_3,
//the std::function's arg define, must obey to the active datastruct,
//for example as follow, arg_1 is const int, arg_2 is int&, arg_3 is const int &, so f2's arg order is "const int + int & + const int &", and also when call f2.
    auto f2 = std::function<int (const int, int &, const int &)>(std::bind(func1, std::placeholders::_3, std::placeholders::_1, std::placeholders::_2));
    std::cout << f2(k, std::ref(j), std::cref(i)) << std::endl;//equal: func1(k, j, i)
    std::cout << k << std::endl;
 
    auto f3 = std::function<int (double)>(std::bind(func2, std::ref(j), std::placeholders::_1, "aaa"));
    std::cout << f3(1.1) << std::endl;
 
    class cls {
        int a;
        int b;
        int c;
 
    public:
        cls(int x, int y, int z):a(x), b(y), c(z){}
        ~cls(){}
 
        void Run() {
            int i = 4;
            auto func = std::function<void (int &)>([this, i] (int &base){
                int times = 8;
                while (times--) {
                    std::this_thread::sleep_for(std::chrono::seconds(1));
                    std::cout << std::fixed << std::setprecision(6) << (a + b + c + base)/i << std::endl;
                    base = a + b + c + base;
                }
            });
 
            int base = 10;
            func(std::ref(base));
        }
    };
 
    cls cl(1, 2, 3);
    cl.Run();
 
 
    class cls2 {
        int a;
        int b;
        int c;
 
    public:
        cls2(int x, int y, int z):a(x), b(y), c(z){}
        ~cls2(){}
 
        void RealRun (int &base) {
            int times = 8;
            while (times--) {
                std::this_thread::sleep_for(std::chrono::seconds(1));
                std::cout << (a + b + c + base) << std::endl;
                base = a + b + c + base;
            }
        }
 
        void Run () {
            int base = 10;
            auto f = std::bind(&cls2::RealRun, this, base);
            f();
        }
    };
 
    cls2 cl2(1, 2, 3);
    cl2.Run();
}
        f1是直接綁定一個普通函數;

        f2是通過std::bind綁定一個普通函數,並且修改了函數傳參順序;

        f3也是通過std::bind綁定一個普通函數,進一步修改了函數傳參個數;

        這里描述下std::bind的傳參個數和順序,通過bind修改傳參的個數或者順序,實現對同一個普通函數實現(包括類成員函數),有任意的自定義傳參的函數變體,如對於下面的函數A:

        函數A (int, double, string) {......},可以實現如下變體:

        f1 = std::bind(&A, 1, 1.1, std::placeholders::_1)                                  //f1("abc")  ==  A(1,1.1,"abc")

        f2 = std::bind(&A, std::placeholders::_2, 1.1, std::placeholders::_1)   //f2("abc",1) ==  A(1,1.1,"abc")

      f3 = std::bind(&A, std::placeholders::_3,std::placeholders::_1,std::placeholders::_2)    //f3(1.1,"abc",1) == A(1,1.1,"abc")

        int b =1;

        f4 = std::bind(&A, b,std::placeholders::_1,"abc")    //f4(1.1) == A(1,1.1,"abc")

        f1、f2、f3對應的函數定義都是函數A的函數定義,但是實現了不同形態的函數調用"變體",方便根據具體情況自定義;

       

        cls類成員函數Run,通過lambda實現閉包;

        cls2類成員函數Run,通過bind另一個成員函數RealRun實現閉包;

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM