zoukankan      html  css  js  c++  java
  • C11简洁之道:lambda表达式

    1、  定义

      lambda表达式是C++11非常重要也是很常用的特性之一,来源于函数式编程的概念,也是现代编程语言的一个特点。它有如下特点:

    • 声明式编程风格:就地匿名定义目标函数或者函数,不需要额外写一个命名函数或者函数对象,以更直接的方式写程序。
    • 简洁:不需要额外再写一个函数或者函数对象,避免了代码膨胀和功能分散。
    • 在需要的时间和地点实现功能闭包,使程序更加灵活。

      lambda表达式定义一个匿名函数,并且可以捕获一定范围内的变量,其定义如下:

      [captrue] (params) opt -> ret {body};

      其中,capture是捕获列表;params是参数列表;opt是函数选项;ret是返回值类型;body是函数体。

      我们来做一个简单的例子,定义一个简单的封包,用来将输入的结果+1并返回。

    auto f = [](int a) -> int {return a + 1; };
    std::cout << f(1) << std::endl;

      lambda表达式的返回值通过返回值后置语法来定义,所以很多时候可以省略返回值类型,编译器根据return语句自动推导返回值类型。

    auto f = [] (int a) {return a + 1;};

      但是初始化列表不能作为返回值的自动推导,需要显示给出具体的返回值类型。

    auto f1 = [] (int a) {return a + 1;};  //ok,return type is int
    auto f2 = [] () {return {1, 2}};    //error:无法推导出返回值类型

      lambda表达式在没有参数列表的时候,参数列表可以省略。

    auto f1 = [] () {return 1;};
    auto f2 = [] {return 1;} //省略空参数表

    2、  捕捉

      lambda表达式可以通过捕获列表捕获一定范围内的变量:

    • []不捕获任何变量;
    • [&]捕获外部作用域所有变量,并作为引用在函数体使用(按引用捕获);
    • [=]捕获外部作用域作用变量,并作为副本在函数体使用(按值捕获);
    • [=,&foo]按值捕获外部作用域所有变量,并按引用捕获foo变量;
    • [bar]按值捕获bar变量,同时不捕获其他变量;
    • [this]捕获当前类中的this指针,让lambda拥有和当前类成员函数同样的访问权限,如果已经使用了&或者=,就默认添加此选项。捕获this的目的是可以在lambda中使用当前类的成员函数和成员变量。
    class A
    {
    public:
        int mi = 0;
    
        void func(int x, int y)
        {
            auto x1 = []{return mi;};                      //error,没有捕获外部变量
            auto x2 = [=] {return mi + x + y;};            //ok,按值捕获所有外部变量
            auto x3 = [&] {return mi + x + y;};            //ok,按引用捕获所有外部变量
            auto x4 = [this] {return mi;};                 //ok,捕获this指针
            auto x5 = [this] {return mi + x + y;};         //error,没有捕获x,y
            auto x6 = [this,x,y] {return mi + x + y;};     //ok,捕获this,x,y
            auto x7 = [this] {return mi++;};               //ok,捕获this指针,并修改成员的值
        }
    };
    
    int a = 0, b = 2;
    auto f1 = [] {return a;} ;                 //error,没有捕获外部变量
    auto f2 = [&] {return a++;};               //ok,按引用捕获所有外部变量,并对a执行自加运算
    auto f3 = [=] {return a;};                 //ok,按值捕获所有外部变量,并返回a
    auto f4 = [=] {return a++;};               //error,按值引用不能改变值
    auto f5 = [a] {return a + b;};             //error,没有捕获b
    auto f6 = [a, &b] {return a + (b++);};     //ok,捕获a和b的值,并对b做自加运算
    auto f7 = [=, &b] {return a + (b++);};     //ok,捕获所有外部变量和b的引用,并对b做自加运算

      从例子中可以看到,lambda表达式的捕获列表精细的控制表达式能够访问的外部变量,以及如何访问这些变量。需要注意的是,默认状态下的lambda表达式无法修改通过复制方式捕获的外部变量,如果希望修改这些变量,需要用引用的方式进行修改。但是按值引用的话,如果延迟调用,那么在调用该lambda表达式的时候,其捕获的变量值还是在lambda定义的时候的值。

    int a = 0;
    auto f = [=] {return a;};      //按值捕获外部变量
    a += 1;                        //修改值
    std::cout << f() << std::endl; //输出0            

      这个例子中,lambda表达式按值捕获了所有外部变量,在捕获的一瞬间a的值就已经被赋值在其中,之后a值修改并不会改变lambda表达中存的a的值,因此最终结果输出还是0。如果希望lambda表达式在调用的时候能即时访问外部变量,应当使用引用的方式捕获。

      如果希望去修改按值捕获的外部变量,需要显示指明lambda表达式为mutable,但是被mutable修饰的lambda表达式有一个特点,就是无论有没有参数都要写明参数列表。

    int a = 0;
    auto f1 = [=] {return a++;}; //error,不能修改按值捕获的变量
    auto f2 = [=] mutable {return a++; }; //ok,mutable

      lambda表达式其实是一个带有operator()的类,即仿函数,因此我们可以使用std::bind和std::function来存储和操作lambda表达式:

    std::function<int(int)> f1 = [] (int a){ return a;};
    std::function<int(void)> f2 = std::bind([](int a) {return a}, 123);

      对于没有捕获任何变量的lambda表达式,还可以被转换成一个普通的函数指针:

    using func_t = int(*)(int);
    func_t f = [](int a){return a;};
    f(123);

      lambda表达式可以说是定义仿函数闭包的语法糖,它捕获的任何外部变量都会转换为闭包类型的成员变量。而使用成员变量的类的operator(),如果能直接转换为普通的函数指针,那lambda表达式本身的this指针会丢失,没有捕获任何外部变量的lambda表达式则不存在这个问题,所以按值捕获的外部变量无法修改。因为lambda表达式中的operator()默认是const的,一个const成员函数无法修改成员变量的值,而mutable则是取消operator()的const。

      所以,没有捕获变量的lambda表达式可以直接转换为函数指针,而捕获变量的lambda表达式则不能转换为函数指针。

    typedef void(*Ptr)(int *);
    Ptr p1 = [](int *p) {delete p;}; //ok,没有捕获的lambda表达式可以转换为函数指针
    Ptr p2 = [&](int *p){delete p;}; //error,有捕获的lambda表达式不能直接转换为函数指针,不能通过编译

    3、  简洁代码

      就地定义匿名函数,不再需要定义函数对象,大大简化了标准库的调用。比如我们使用for_each将vector中的偶数打印出来。

    class CountEven
    {
    private:
        int &micount;
    
    public:
        CountEven(int &count) : micount(count) {}
    
        void operator()(int val)
        {
            if(!(val & 1))
            {
                ++micount;
            }
        }       
    };
    
    std::vector<int> v = {1, 2, 3, 4, 5, 6};
    int count = 0;
    for_each(v.begin(), v.end(), CountEven(count));
    std::cout << "The number:" << count << std::endl;

      这样写比较繁琐,如果用lambda表达式,使用闭包概念来替换这里的仿函数。

    std::vector<int> v = {1, 2, 3, 4, 5, 6};
    int count = 0;
    
    for_each(v.begin(), v.end(), [&count] (int val)
    {
        if(!(val & 1))
        {
            ++count;
        }
    });

      在之前的例子中,使用std::bind组合多个函数,实现了计算集合中大于5小于10的元素个数。

    using std::placeholders::_1;
    auto f = std::bind(std::logical_and<bool>(),
    std::bind(std::greater<int>(), std::placeholders::_1, 5),
    std::bind(std::less_equal<int>(), std::placeholders::_1, 10));
    int count = std::count_if(coll.begin(), coll.end(), f);

      通过lambda表达式可以轻松的实现类似的功能:

      //查找大于5小于10的元素个数

    int count  = std::count_if(coll.begin(), coll.end(), [](int x) {return x > 5 && x < 10;})

      lambda表达式比std::bind更加灵活和简洁,如果简单的逻辑处理,用lambda表达式来代替function,提升开发效率,效果会更好。

  • 相关阅读:
    字符编码与解码详解
    【Java反射机制】用反射改进简单工厂模式设计
    数据结构
    根据 中序遍历 和 后序遍历构造树(Presentation)(C++)
    【动态规划】记忆搜索(C++)
    Linux环境下安装中山大学东校区iNode客户端
    webpack前端开发环境搭建
    CSS中line-height继承问题
    MySQL中MyISAM与InnoDB的主要区别对比
    JavaScript中易混淆的DOM属性及方法对比
  • 原文地址:https://www.cnblogs.com/ChinaHook/p/7658443.html
Copyright © 2011-2022 走看看