暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Lambda表达式

一个程序员的修炼之路 2021-10-04
278

各位国庆节快乐,祝祖国繁荣昌盛!

常见的语言中都提供Lambda语法糖,比如C#, Python, Golang等。本文将探讨下C++ 11引入的Lambda语法糖。语法糖
是一种让程序员使用更加便利的一种语法,并不会带来额外的功能,比如Lambda
,没有这种语法糖,其可以用已有的语法等价的实现出相应的功能。
有编程实践经验的同学一定能够快速的理解Lamdba
产生的意义,而缺乏编程经验的同学,跟着我一起来梳理下Lamdba
给我们带来了哪些便利性?


函数指针和对象函数

因为笔者用Lambda
最多的场景是回调函数,先说说回调函数。在编程中回调函数
是一个常见的设计方式, 下图是一个常见的同步调用的回调函数:

  1. 调用方
    访问被调用方
    的实现函数InvokeFunction

  2. 被调用方
    访问调用方
    的回调函数CallbackFunction

上述是一个同步调用的回调方式,是实践中,也有可能是一个异步的回调方式。
一般回调的使用场景可以是被调用方
使用调用方
指定的方法去实现内部的一个逻辑。常见的比如:

  1. 被调用模块
    使用调用模块
    指定的方法完成其功能,比如常见的std::sort

  2. 比如SDK没有写DebugLog的功能,而是通过回调函数的方式,让调用方实现写DebugLog功能。

  3. 通知机制:在一些场景下,被调用方
    通过回调函数去通知调用模块
    ,去进行相应操作。

回调的场景应该不止上述描述的这些,这一章节的重点让我们回归到回调函数
函数对象
仿函数
)。

回调函数最常见的C和C++中都使用的函数指针
,我们以std::sort
为例。一个vector
容器中存储了若干的Student
信息,想要将这些学生信息根据年龄进行升序排序,于是可以调用std::sort
,并且使用自定义的函数StudentSortFunction
sort
作为回调函数来完成排序。

    #include <algorithm>
    #include <iostream>
    #include <vector>


    struct Student
    {
    std::string m_strName;
    unsigned int m_uAge;
    };


    void PrintStudentVector(const std::vector<Student>& vecStudents)
    {
    for (auto&& student : vecStudents)
    {
    std::cout << student.m_strName.c_str() << ":" << student.m_uAge << std::endl;
    }
    std::cout << std::endl;
    }


    bool StudentSortFunction(const Student& student1, const Student& student2)
    {
    return student1.m_uAge < student2.m_uAge;
    }


    int main()
    {
    std::vector<Student> vecStudents= {
    {"xiaoqiang", 15},
    {"xiaoming", 13},
    {"xiaoke", 13}
    };


    PrintStudentVector(vecStudents);


    std::sort(vecStudents.begin(), vecStudents.end(), StudentSortFunction);


    //Print after sort
    PrintStudentVector(vecStudents);
    return 0;
    }
    复制

    C++中有了函数对象概念,我们同样以上述的例子,实现了一个函数对象StudentSort
    ,其包含一个重载的函数接口bool operator() (const Student& student1, const Student& student2)
    ,同样可以实现同样的功能。

      #include <algorithm>
      #include <iostream>
      #include <vector>


      struct Student
      {
      std::string m_strName;
      unsigned int m_uAge;
      };


      void PrintStudentVector(const std::vector<Student>& vecStudents)
      {
      for (auto&& student : vecStudents)
      {
      std::cout << student.m_strName.c_str() << ":" << student.m_uAge << std::endl;
      }
      std::cout << std::endl;
      }


      class StudentSort
      {
      public:
      bool operator() (const Student& student1, const Student& student2)
      {
      return student1.m_uAge < student2.m_uAge;
      }
      };


      int main()
      {
      std::vector<Student> vecStudents= {
      {"xiaoqiang", 15},
      {"xiaoming", 13},
      {"xiaoke", 13}
      };


      PrintStudentVector(vecStudents);


      std::sort(vecStudents.begin(), vecStudents.end(), StudentSort());


      //Print after sort
      PrintStudentVector(vecStudents);
      return 0;
      }
      复制

      当然上述的例子函数指针
      函数对象
      似乎没有太多区别。我们注意看回调的方法的入参是由被调用方
      给定的并且传入的。但是在一些场景,我们是需要在回调方法
      中同样传入被调用方的一些信息。这个时候的回调方法一般的形式是, 会传入一个pCtx
      ,其存储调用方
      所需要传递给回调函数的一些信息。

        void CallbackFunction(Contex* pCtx, Parameter par1, Parameter par2.....)
        复制

        在这种情况下函数指针
        函数对象
        就有了区别了,函数指针
        是没有成员的,而函数对象
        是可有成员函数的,这个时候在C++中,回调的方法一般采用函数对象来实现上述的方式, 比如定义了一个回调函数对象CallbackContext callbackContext
        设置给被调用方
        被调用方
        使用callbackContext(par1, par2)
        即完成了回调方法的调用。

          class CallbackContex
          {
          public:
          bool operator() (Parameter par1, Parameter par2) { ; };
          private:
          Contex* m_pCtx;
          };
          复制

          那么也就是说,每次我们设置给被调用
          放都需要定义一个class
          ,将调用方需要设置给被调用方
          的变量给打包到一个叫做Contex
          中,这个时候手写一个函数对象,感觉比较繁琐。注意只是繁琐,而不是无法实现。
          这个时候使用Lambda
          来实现就显的十分的方便快捷了,因为其有一个很棒的功能,叫做捕获变量
          。接下来让我们一起来看看本文的主角lambda
          吧。


          Lambda

          Lambda
          的表达式如上图所示,其主要构成部分就比普通的函数多了一个捕获列表
          ,主要由5个部分构成。

          1. 捕获列表,其可以捕获当前上下文的变量,可以是值捕获或者引用捕获

          2. 函数参数,不用赘述,和普通函数一样

          3. specifiers, 可选的,主要说明下mutable
            , 默认情况下值捕获,将无法修改其值(可以想象为其成员函数后面跟了个const
            ),除非设置为mutable
            .

          4. 返回值,如果不写表示返回void

          5. 函数体, 这部分可以使用你捕获列表里面的变量,也可以使用参数列表里面的变量。

          看到这里是不是来演练下第一章节的例子,使用Lambda
          如何更简洁的写出一个排序的回调, 是不是比较简单。

            std::sort(vecStudents.begin(), vecStudents.end(), [](const Student& student1, const Student& student2) -> bool {
            return student1.m_uAge < student2.m_uAge;
            });
            复制

            Lambda
            的表达式的结果(注意不是返回值)是一个匿名函数对象,我们一般可以使用 auto
            来获取其表达式结果,同样也可以使用。std::function<T>

            下面我们来举个例子让我们来更加好的理解Lambda
            , 尤其是值捕获
            引用捕获

              #include <iostream>


              int main()
              {
              unsigned int uYear = 2020;
              unsigned int uMonth = 9;


              std::cout << "uYear: " << uYear
              << " Month: " << uMonth << std::endl << std::endl;


              auto lambda = [&uYear, uMonth]() -> bool {
              uYear = 2021;
              std::cout << "uYear: " << uYear
              << " Month: " << uMonth << std::endl << std::endl;


              //error C3491: 'uMonth': a by copy capture cannot be modified in a non-mutable lambda
              //uMonth = 10;


              return true;
              };

              lambda();


              std::cout << "uYear: " << uYear
              << " Month: " << uMonth << std::endl << std::endl;
              return 0;
              }
              复制

              这个例子我们可以看到在Lambda
              中使用引用捕获
              uYear
              值捕获
              uMonth
              。那么在Lambda
              函数体内:

              • uYear
                main
                函数中的uYear
                的引用,对uYear
                的重新复制为2021
                也会影响到main
                uYear

              • uMonth
                只是main
                函数中的uMonth
                的值传递,默认情况下不能够直接进行改写,除非将Lambda
                指定为mutable
                。如果其为mutable
                , 在函数体内的修改并不会影响main
                uMonth
                的改变。

              其实上述的Lamdba
              表达式可以用下面的类来表达其含义, 这样的表达易于读者去理解Lambda
              在编译器中的实现,也能够更好的掌握Lambda

                class LambdaClass_XXXXX
                {
                public:
                LambdaClass_XXXXX(unsigned int& uYear, unsigned int uMonth) :m_uYear(uYear), m_uMonth(uMonth) {}
                bool operator()() const
                {
                m_uYear = 2021;
                std::cout << "uYear: " << m_uYear
                << " Month: " << m_uMonth << std::endl << std::endl;


                return true;
                }
                private:
                unsigned int& m_uYear;
                unsigned int m_uMonth;
                };
                复制

                LambdaClass_XXXXX
                的命名方式是避免的名字冲突。实际可以查看编译器MSVC命名的方式如下图所示:

                如果有很多的参数需要捕获,Lambda
                也提供了一些简便的方式:

                • [&, uMonth]
                   表示uMonth
                  采用值捕获
                  ,其他可见的变量均采用`引用捕获

                • [=, &uYear]
                   表示uYear
                  采用引用捕获
                  ,其他可见的变量均采用值捕获


                那么如果捕获列表的变量名字和函数参数名字相同呢? ,我试了几个不同的编译器,结果不相同,有的报错,有的优先选择函数参数,有的优先选择捕获列表。总之使用者尽量避开名字相同的问题。关于这个在Stackoverflow
                上也有所讨论: <<Lambda capture and parameter with same name - who shadows the other? (clang vs gcc)>>, 个人的角度来说更希望是编译阶段直接报错。

                通过这一章节的内容,你是否能够举一反三了呢?出一道题目给读者做一做吧。


                给读者的问题

                为了更好的让读者理解Lambda
                的实现,请问以下的程序结果输出是什么呢?先想一个答案,然后不确定的同学用编译器跑了试一试吧。如果答案错误,欢迎和笔者一起讨论哦。

                  #include <iostream>


                  int main()
                  {
                  int iVal = 100;
                  auto lambda = [iVal]() mutable {
                  iVal += 100;
                  std::cout << iVal << std::endl;
                  };
                  lambda();
                  lambda();
                  return 0;
                  }
                  复制


                  总结

                  Lambda
                  是一种让C++
                  对象函数编写更加便利的语法糖,在使用Lambda
                  的时候一定要理解其实现原理,尤其是捕获列表
                  值捕获
                  引用捕获
                  , 以及要注意其生命周期,以防非法的内存访问导致程序出错。另一点就是文中提到的一个注意点,尽量避免捕获列表的变量名称和函数参数的变量名称相同的情况,因为当前的不同编译器的实现不同,否则掉进坑里了哦。


                  文章转载自一个程序员的修炼之路,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                  评论