第13课 lambda表达式

Posted 浅墨浓香

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了第13课 lambda表达式相关的知识,希望对你有一定的参考价值。

1. lambda语法形式[capture](params) opt -> ret {body;};

(1)capture为捕获列表

  ①[]、[&]和[=]分别表示不捕获、按引用捕获、按值捕获所有父作用域中内的局部变量。(父作用域指包含lambda表达式的语句块,如main函数)。

    ◆lambda函数只能捕获父作用域中的局部变量,而捕获非父作用域static变量都会报错(不是C++11的标准,其行为可能因编译器而不同)。(注意全局变量或static变量不能被捕获即不能被写入捕获列表中,但可在lambda的函数体内直接访问

    ◆默认下无法修改按值捕获的外部变量因为lambda表达式的operator()默认是const函数,但捕获this指针后可修改成员变量的值,因为是通过this指针来修改的)。

    ◆在类中如果使用&或=捕获,会同时默认捕获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
{
private:
    int i = 0;
public:
    
    void func(int x, int y)
    {
        //auto x1 = []{return i;};   //error,没有捕获任何变量。当然也无法捕获父作用域(func)以外的变量,因此i是不能访问到的!
        //auto x1 = [&i]{return i;}; //error,无法捕获父作用域(func)以外的变量
        auto x1 = [=]{return i++;}; //ok,因为“=”或“&”默认会捕获this指针,也就可以访问到成员变量。根据按值捕获的特点,此时
                                    //在lambda的函数体内不能修改this指针本身。但这不影响我们通过this指针修改成员变量i的值!
        
        auto x2 = []{return g++;};  //ok,g不在lambda的父作用域,不能被捕获。
                                    //如auto x2 = [&g]{return g++;}。但由于g是全局变量
                                    //所以在lambda的body内仍是可见的!
                                    
        auto x3 = [=]{return i++, i + x + y;};//ok,按值捕获所有外部变量,由于&或=捕获时会默认地同时传入this,所以可改变i的值
        auto x4 = [&]{return i + x + y;}; //ok,按引用捕获所有变量:x, y以及this指针
                                          //但注意,i没有被捕获,也不可捕获到。它是通过this指针来访问到的。
                                          
        auto x5 = [this]{return i++;};    //ok,捕获了this指针,也就可以修改成员的值
        //auto x6 = [this, x]{return i + x + y;}; //error,没捕获y
    }
};

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表达式的主要内容,如果未能解决你的问题,请参考以下文章

第13课 右值引用

一课掌握Lambda表达式语法及作用简单入门1??

第18课 捕获机制及陷阱

行为参数化与lambda表达式 - 读《Java 8实战》

IntelliJ:求值lambda表达式在调试时引发编译错误

Java-Lambda表达式第一篇认识Lambda表达式