• C++11新特性⑤ | 仿函数与lambda表达式


    目录

    1、引言

    2、仿函数

    3、lambda表达式

    3.1、lambda表达式的一般形式

    3.2、返回类型说明

    3.3、捕获列表的规则

    3.4、可以捕获哪些变量

    3.5、lambda表达式给编程带来的便利


    VC++常用功能开发汇总(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/124272585C++软件异常排查从入门到精通系列教程(专栏文章列表,欢迎订阅,持续更新...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/125529931C++软件分析工具从入门到精通案例集锦(专栏文章正在更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/131405795C/C++基础与进阶(专栏文章,持续更新中...)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/category_11931267.html       C++11新特性很重要,作为C++开发人员很有必要去学习,不仅笔试面试时会涉及到,开源代码中也在大规模的使用。以很多视频会议及直播软件都在使用的开源WebRTC项目为例,WebRTC代码中大篇幅地使用了C++11及以上的新特性,要读懂其源码,必须要了解这些C++的新特性。所以,接下来一段时间我将结合工作实践,给大家详细讲解一下C++11的新特性,以供借鉴或参考。

    1、引言

           在C++中我们可以使用函数名、函数指针、仿函数去实现函数的调用。C++11引入了lambda表达式,又称匿名函数,给我们引入了一种新的函数调用方式,给我们编程带来了很大的便利。今天我们来讲讲仿函数和lambda表达式。

    2、仿函数

           仿函数是一种特殊的类或结构体,不是C++11引入的,之前就有了。它重载了函数调用运算符operator(),并且可以像函数一样被调用,同时它也可以拥有自己的数据成员和成员函数。仿函数通常用于算法(如sort、find、transform等)和容器(如set、map、list等)中,以提供自定义的操作行为。在C++11中,可以使用lambda表达式实现简单的仿函数。

           下面是个类中重载 operator()的实例:

    1. class MyFunctor
    2. {
    3. public:
    4.     MyFunctor(int tmp) : round(tmp) {}
    5.     int operator()(int tmp) { return tmp + round; }
    6. private:
    7.     int round;
    8. };
    9. int main()
    10. {
    11.     int round = 2;
    12.     MyFunctor f(round); //调用构造函数
    13.     cout << "result = " << f(1) << endl; // operator()(int tmp)
    14.     return 0;
    15. }

    通过类对象去调用重载的operator()方法。 在C++里,我们通过在一个类中重载operator()运算符的方法,去使用一个函数对象而不是一个普通函数。

           再看一个比较数大小的实例:

    1. class compare_class
    2. {
    3.     public:
    4.     bool operator() (int A, int B) const{return A < B;}
    5. };
    6.  
    7. // Declaration of C++ sorting function.
    8. template<class ComparisonFunctor>
    9. void sort_ints(int* begin_items, int num_items, ComparisonFunctor c);
    10.  
    11. int main()
    12. {
    13.     int items[]={4, 3, 1, 2};
    14.     compare_class functor;
    15.     sort_ints( items, sizeof(items)/sizeof(items[0]), functor);
    16. }

    3、lambda表达式

           lambda表达式,又称匿名函数,是一个可调用的代码单元,可以理解为一个未命名(匿名)的内联函数。与一般的函数类似,lambda表示式有一个返回类型、一个参数列表和一个函数体。但和函数不同的是,lambda表达式是直接定义在函数内部。

           lambda表达式的引入,给我们编程带来了很大的便利,可以大大简化我们的编码!

    3.1、lambda表达式的一般形式

    lambda表达式的一般形式如下:

    [capture list]( parameter list ) -> return type { function body }

    其中:

    1)capture list(捕获列表),是一个本表达式所在函数的局部变量的列表;
    2)parameter list(参数列表),是给本lambda表达式传入的参数列表;
    3)return type(返回类型),是本lambda表达式的返回值类型(可省略);
    4)function body(函数体),是本lambda表达式的内部实现。

           对于普通函数,返回类型位于函数开始处,但lambda表达式因为其形式,返回类型不能放在开始处,必须使用尾置的方式来指定返回类型。我们可以忽略参数列表和返回值类型,但必须包含捕获列表和函数体,比如:

    auto f = [] { return 20; };

    3.2、返回类型说明

           直接在lambda表达式中指定返回类型,没什么问题。下面我们看看没指定返回类型时,将会发生什么。

           如果lambda表达式的函数体中只包含一个return语句,如果没指定lambda表达式的返回值,则编译时会根据return语句中的内容推导出本表达式的返回值类型,比如实现两个整型数据相加的lambda表达式:

    [] ( int a, int b) { return a + b; }

    本lambda中没有指定函数返回类型,根据表达式(a+b)可以推断出本lambda返回值类型为int。

           如果lambda表示式中不是只包含单一的return语句(多条语句),编译时编译器认定该lambda返回值类型为void。比如返回一个数绝对值的lambda如下:

    [] ( int a ) { if ( a < 0) return -a; else return a; };

    因为包含了多条语句,所以编译器认定该lambda返回void,编译时会报错!因为返回类型为void,却使用return返回了int类型,不一致了,所以报错!

    3.3、捕获列表的规则

           捕获列表,涉及到访问所在函数哪些局部变量,以及访问变量的方式。访问变量的方式主要有以引用的方式访问,还是以值的方式访问。如果是将lambda要访问的变量在捕获列表中罗列出来,则是显示捕获;如果不罗列,就是隐式捕获。

           下面给出完整的捕获列表规则:

    捕获类型描述
    []空捕获列表。lamda表达式内部不使用所在函数中的变量。只有捕获列表不为空才能使用所在函数中的变量。
    [names]names是一个逗号分割的名字列表,这些名字是lamda所在函数的局部变量的名称。默认情况下,捕获列表中的变量值被拷贝,变量前面可以添加&,加&则表示采用引用捕获方式。
    [&]隐式捕获列表,让编译器根据lamda内部的代码去推断使用了所在函数的哪些局部变量。采用引用捕获方式,lamda体中所使用的来自所在函数的变量都采用引用方式使用。
    [=]隐式捕获列表,让编译器根据lamda内部的代码去推断使用了所在函数的哪些局部变量。采用值捕获方式,lamda体中将拷贝所使用的来自所在函数的变量的值。
    [&, identifier_list] identifier_list(理解为不使用&引用捕获的特例)是逗号分割的列表,包含0个或多个来自所在函数的变量,这些变量采用值捕获,identifier_list列表中的名字前面不能加&。而除identifier_list列表中指明的变量之外的任何隐式捕获的变量都采用引用捕获方式。
    [=, identifier_list]identifier_listt(理解为不使用=值捕获的特例)是逗号分割的列表,包含0个或多个来自所在函数的变量,这些变量采用引用捕获,identifier_list列表中的名字前面必须使用&。而除identifier_list列表中指明的变量之外任何隐式捕获的变量都采用值捕获方式。

    关于捕获列表的例子,如下:

    1. int main()
    2. {
    3.     int a = 0, b = 1;
    4.     auto f1 = []{ return a; };      // error, 没有捕获外部变量
    5.     auto f2 = [=]{ return a; };     // ok, 值传递方式捕获所有外部变量
    6.     auto f3 = [=]{ return a++; };   // error, a是以赋值方式捕获的,无法修改
    7.     auto f4 = [=]() mutable { return a++; };   // ok, 加上mutable修饰符后,可以修改按值传递进来的拷贝
    8.     auto f5 = [&]{ return a++; };              // ok, 引用传递方式捕获所有外部变量, 并对a执行自加运算
    9.     auto f6 = [a]{ return a+b; };              // error, 没有捕获变量b
    10.     auto f9 = [a,&b]{ return a+(b++); };       // ok, 捕获a, &b
    11.     auto f8 = [=,&b]{ return a+(b++); };       // ok, 捕获所有外部变量,&b
    12.     auto f9 = [&,&b]{ return a+(b++); };       // error, 捕获所有外部变量,不能使用&b,b是特例,和默认的引用捕获不一样,使用值捕获,所以不能加&
    13.     auto f10 = [=,b]{ return a+(b++); };       // error, 捕获所有外部变量,不能使用b,b是特例,和默认的值引用不一样,使用引用捕获,前面必须加&
    14.     return 0;
    15. }

    3.4、可以捕获哪些变量

           一般lambda表达式是放置在一个函数中的,即在函数中嵌入的,lambda函数实现体中可以访问其所在函数的局部变量。如果lambda表达式所在函数是一个类的成员函数,则lambda内部也可以访问当前函数所在的类的成员变量,这一点可能很多人不知道。比如:

    1. class Test
    2. {
    3. public:
    4.     int i = 0;   // 类的成员变量
    5.     void func(int x, int y)
    6.     {
    7.         auto x1 = []{ return i; };          // error, 没有捕获外部变量
    8.         auto x2 = [=]{ return i+x+y; };     // ok, 值传递方式捕获所有外部变量
    9.         auto x3 = [=]{ return i+x+y; };     // ok, 引用传递方式捕获所有外部变量
    10.         auto x4 = [this]{ return i; };      // ok, 捕获this指针
    11.         auto x5 = [this]{ return i+x+y; };  // error, 没有捕获x, y
    12.         auto x6 = [this, x, y]{ return i+x+y; };// ok, 捕获this指针, x, y
    13.         auto x9 = [this]{ return i++; };        // ok, 捕获this指针, 并修改成员的值
    14.     }
    15. };

    3.5、lambda表达式给编程带来的便利

           lambda表达式的引入,给我们编程带来了很大的便利,可以大大简化我的编码。比如我们在使用STL容器的find_if、count_if、sort等算法函数时,我们要传入条件函数或者比较函数,这些函数直接用lambda表达式去实现,要方便很多。
           在以前没有lambda表达式时,这些条件函数和比较函数,需要在函数外实现,要不定义成全局函数,要不定义成静态函数,甚至还要定义一些辅助的全部或者静态变量。比如有一个存放设备信息的结构体,然后有个存放设备信息的vector列表,给该列表简单的初始化一下:

    1. // 设备信息结构体
    2. typedef struct tagDeviceInfo
    3. {
    4.     char szDeviceId[64];   // 设备id
    5.     char szDeviceName[64]; // 设备名称
    6.     int nDevType;          // 设备类型
    7. public:
    8.     tagDeviceInfo(){ memset(this, 0, sizeof(tagDeviceInfo)); }
    9. }TDeviceInfo;
    10. // 设备管理类
    11. class CDeviceManage
    12. {
    13.     CDeviceManage();
    14.     ~CDeviceManage();
    15.     void InitDeviceList();
    16. private:
    17.     vector m_vtDevList;
    18. }
    19. // 初始化设备列表(仅用于测试,随意初始化了一些数据)
    20. void CDeviceManage::InitDeviceList()
    21. {
    22.     for ( int i = 0; i < 10; i++ )
    23.     {
    24.         TDeviceInfo tDevInfo;
    25.         char szBuf[128] = { 0 };
    26.         sprintf(szBuf, "E40CF3E4-CC2B-437F-A4B9-65F2D5BD071%d", i);
    27.                                    strcpy(tDevInfo.szDeviceId, szBuf);
    28.         CUIString strName;
    29.         sprintf(szBuf, "设备%d", i);
    30.                                    strcpy(tDevInfo.szDeviceName, szBuf);
    31.         m_vtDevList;.push_back(tDevInfo);
    32.     }
    33. }

    假设我们在CDeviceManage::FindTest成员函数中调用STL的算法函数find_if到m_vtDevList列表中搜索设备Guid(szDeviceId)为E40CF3E4-CC2B-437F-A4B9-65F2D5BD0715的设备信息。如果不使用lambda表达式,则要将条件函数实现在CDeviceManage类外部定义成全局函数,同时要将存放目标id的变量定义成全局的,如下所示:

    1. char* s_lpszTargetDevId = ""; // 定义成静态变量
    2. // 将条件匹配函数定义在类CDeviceManage外部,定义成全局函数
    3. BOOL MatchFunc(TDeviceInfo& tDevInfo)
    4. {
    5.     return strcmp( s_lpszTargetDevId,tDevInfo.szDeviceId ) == 0;
    6. }
    7. bool CDeviceManage::FindTest()
    8. {
    9.      // 对静态变量s_pTargetDevId进行赋值
    10.      s_lpszTargetDevId = "E40CF3E4-CC2B-437F-A4B9-65F2D5BD0715";
    11.      vector::iterator itor = std::find_if(m_vtDevList.begin(), m_vtDevList.end(), MatchFunc);
    12.      if ( itor != m_vtDevList.end() )
    13.      {
    14.          // 找到对应的设备信息,进行后续处理的代码省略
    15.          // ......
    16.          return true;
    17.      }
    18.      return false;
    19. }

    上述代码实现的相对麻烦一些。
           如果使用lambda表达式,代码要简洁很多,不用将条件匹配函数定义成全局函数,也不用定义静态辅助变量s_lpszTargetDevId,用lambda表达式实现的代码如下:

    1. bool CDeviceManage::FindTest()
    2. {
    3.     char* lpszTargetDevId = "E40CF3E4-CC2B-437F-A4B9-65F2D5BD0715";
    4.     vector::iterator itor = std::find_if(vtDevList.begin(), vtDevList.end(), [=](const TDeviceInfo& tDevInfo){
    5.     return strcmp(lpszTargetDevId, tDevInfo.achDeviceId) == 0; } );
    6.     // 后续代码省略
    7.     // ......
    8. }

            至于为什么要使用STL的算法函数去搜索, 因为STL的算法函数的效率比较高,比直接去for循环遍历效率会高很多!如果STL列表中存放了大量的数据,数据搜索就要讲究效率了,就要使用到STL算法函数,比直接for循环变量要高上几个数量级,这点在项目中对比测试过!关于使用STL算法函数提高搜索效率的文章,可以参见我之前写的文章:

    VC++调用STL算法函数有效提升STL列表的搜索速度(附源码)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/123943134VC++如何使用C++ STL标准模板库中的算法函数(附源码)icon-default.png?t=N7T8https://blog.csdn.net/chenlycly/article/details/125486409

  • 相关阅读:
    【Redis】五大常见的数据类型之 List
    关于倾斜摄影测量技术,你了解多少?
    【java】【SSM框架系列】【一】Spring
    业务架构、应用架构、技术架构、数据架构
    系统架构设计专业技能 ·操作系统
    服务器怎么买,腾讯云服务器新手购买的流程方法步骤
    Ubuntu1804里进行KITTI数据集可视化操作
    一文读懂css【css3】绝对(absolute)定位和相对(relative)定位 相对定位是相对谁定位的 绝对定位又是根据谁绝对定位的 子绝父相 包含块
    802.1Qbb
    链表| leecode刷题笔记
  • 原文地址:https://blog.csdn.net/chenlycly/article/details/132776343