【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器

作者:一只大喵咪1201
专栏:《C++学习》
格言:你只管努力,剩下的交给时间!
【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第1张图片

lambda表达式 | 可变参数模板 | 包装器

  • lambda表达式
    • lambda表达式语法
    • 函数对象与lambda表达式
  • 可变参数模板
    • 展开参数包
    • STL中emplace相关接口
  • 包装器
    • function
    • bind
  • 总结

lambda表达式

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第2张图片
用一个类来描述水果,包括水果的名字,价钱,水果的评分。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第3张图片
将该类实例化出来,进行排序,分别按照价格和评分升序排序。

  • 使用sort()进行排序时,传入比较方法。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第4张图片
使用两个仿函数分别实现价格和评价升序排序的比较逻辑。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第5张图片
运行结果如上图所示,符合我们预期。

  • 如果此时还想根据其他数据排序呢?比如名称,重量等等。

每多一种排序方式就需要写一个仿函数,而且该仿函数只有这里会使用一次,之后就不再使用了。

  • 当排序方式多了以后,而且仿函数命名不是很规范,比如是Compare1,conpare2……

此时程序的可读性就会降低,需要反复确认当前使用的Comare是根据什么比较的,而且很冗余。

C++11中提供了lambda表达式。

见lambda表达式:

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第6张图片
上图红色框中所示就是lambda表达式。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第7张图片
同样可以实现使用仿函数的比较结果。

  • lambda表达式就像是是一个匿名仿函数对象,只在当前位置使用,使用完毕后销毁。
  • 而且增加了代码的可读性,可以直接看到sort的比较逻辑。

lambda表达式语法

lambda表达式书写格式:

[捕捉列表](参数列表)mutable->(返回值类型){函数体}
  • [捕捉列表]

捕捉列表是编译器判断lambda表达式的依据,所以必须写[],[]内可以有参数,后面详细讲解。

  • (参数列表)

参数列表和普通函数的参数列表一样,如果不需要参数传递,可以连同()一起省略。

  • mutable

默认情况下,lambda表达式的形参都是const类型,形参不可以被修改,使用mutable可以取消形参的常量属性。使用mutable时,参数列表不可以省略(即使参数为空)。一般情况下mutable都是省略的

  • ->返回值类型

->和返回值类型是一体的,如->int表示lambda的返回值是int类型。一般情况下省略->返回值类型,因为编译器可以根据函数体中的return推导出返回值类型。为了提高程序的可读性可以写上

  • {函数体}

和普通函数一样,{}里的是lambda的具体实现逻辑。{函数体}里的内容可以写在一行:

{表达式1;表达式2;//.......}

也可以写成多行:

{
	表达式1;
	表达式2;
	//.....
}

注意:

  • 在lambda函数定义中,参数列表和返回值类型都是可选部分,可写可不写。
  • 而捕捉列表和函数体必须写,但是内容可以为空。

lambda表达式的最简形式为:

[]{}

该lambda不能做任何事情。

捕获列表说明:

捕捉列表描述了上下文中那些数据可以被lambda使用,以及使用的方式传值还是传引用:

  • [var]:表示值传递方式捕捉变量var。
  • [=]:表示值传递方式捕获所有父作用域中的变量(包括this)。
  • [&var]:表示引用传递捕捉变量var。
  • [&]:表示引用传递捕捉所有父作用域中的变量(包括this)。
  • [this]:表示值传递方式捕捉当前的this指针。

说明:

  • 父作用域指包含lambda函数的语句块,而且只会捕捉lambda函数前面的父作用域中的变量。
  • &var不能表示取地址,这里是捕捉,并不是传参,捕捉中就不存在取地址这一语法。
  • 语法上捕捉列表可由多个捕捉项组成,并以逗号分割,比如:
  • [=, &a, &b]:以引用传递的方式捕捉变量a和b,值传递方式捕捉其他所有变量
  • [&,a, this]:值传递方式捕捉变量a和this,引用方式捕捉其他变量
  • 捕捉列表不允许变量重复传递,否则就会导致编译错误,比如:
  • [=, a]:=已经以值传递方式捕捉了所有变量,捕捉a重复就会报错。
  • 在块作用域中的lambda函数仅能捕捉父作用域中局部变量,捕捉任何非此作用域或者非局部变量都 会导致编译报错。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第8张图片
将lambda表达式对象赋值给func1,这里暂时使用auto推演类型。

  • 省略了参数列表和返回值类型,函数体中使用的是父域中的变量a和b。
  • 调用lambda时不用传参。

可以看到,lambda执行的逻辑是a+b。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第9张图片

  • 省略返回值类型,且函数体中没有返回值
  • 函数体中使用的是父域中b的引用。

可以看到,变量b在lambda中被改变了。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第10张图片

  • 完整的lambda表达式,省略了mutable。
  • 传值捕捉父域中的所有值,其中变量b是传引用捕捉。
  • 写明返回值类型是int。

可以看到,lambda表达式参数传10以后,函数体进行了运算。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第11张图片

  • 传值捕捉x,使用了mutable,否则在函数体中的捕捉到的x不能被改变。
  • 父域中的x没有被改变,传值捕捉相当于是给形参传值,是一种拷贝,但是默认情况下拷贝的值是const类型,所以需要mutable来改变属性。

可以看到,最终返回的值是函数体中的运算结果。

从上面的例子中也可以看出,lambda表达式本质上可以理解为匿名函数对象,它是一个可调用对象。

函数对象与lambda表达式

函数对象,又称为仿函数,即可以像函数一样使用的对象,就是在类中重载了operator()运算符的类对象。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第12张图片
使用函数对象和lambda两种方式进行利率计算,执行的函数体内容相同。

  • 从使用方式上来看,函数对象和lambda表达式完全一样,如上图中r1和r2所示。
  • rate是函数对象的成员变量,通过构造函数初始化,lambda通过捕获列表来捕获该变量。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第13张图片
调试起来后,查看汇编代码,如上图所示是调用函数对象部分的汇编代码。

  • 创建函数对象时,调用了Rate类域中的构造函数。
  • 调用函数对象时,调用了Rate类域中的operator()成员函数。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第14张图片
上图所示是lambda表达式部分的汇编代码。

  • 创建lambda表达式时,也是调用了某个类中的构造函数。

该类不像函数对象那样明确,而是有很长一串,如上图所示的lambda_0d841c589991fabbf3e571d463f613ab。

  • 调用lambda表达式时,调用的是该类中的operator()成员函数。

函数对象和lambda在汇编代码上是一样的,只是类不同而已。函数对象的类名是我们自己定义的。

  • lambda的类名是编译器自己生成的。

编译器在遇到lambda表达式的时候,会执行一个算法,生成长串数字,而且几乎每次生成的数字都不同,也就意味着每次创建的类名都不同。

  • lambda表达式的类型只有编译器自己知道,用户是无法知道的。
  • 所以要通过auto来推演它的类型,才能接收这个匿名的函数对象。

lambda表达式和函数对象其实是一回事,只是lambda表达式的类是由编译器自动生成的

此时大家应该就理解了为什么说lambda其实就是一个匿名的函数对象了吧。

注意:lambda表达式相互之间不可以赋值,因为编译器生成的类不一样,也就意味着不是一个类型。

可变参数模板

template <class ...Args>
void ShowList(Args... args)
{
	//.......
}
  • Args:是一个模板参数包,在模板中必须以…Args表面它是一个模板参数包。
  • args:是一个形参参数包,它的类型是Args…表面它是一个参数包。
  • 参数包:可以包含0~N个类型的参数。

在使用可变参数模板的时候,可以传入任意个类型的数据,编译器会将所有类型打包。

可变参数模板的难点就是如果展开参数包,从而使用里面的每个模板参数。

展开参数包

递归函数方式展开参数包:

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第15张图片
先直接看结果,调用同一个函数模板,传入不同个数的参数,函数模板都能将这些变化的参数打印出来。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第16张图片
以调用ShowList(1, 1, ‘A’, “sort”);为例解释展开参数包的过程。

  • 传递实参(1, 1, ‘A’, “sort”),ShowList接收参数。
  • 1 → T val
  • (1, ‘A’, “sort”) → Args… args

打印val值,剩下的参数以参数包的形式继续传给ShowList。

  • 传递实参(1, ‘A’, “sort”),ShowList接收参数。
  • 1 → T val
  • (‘A’, “sort”) → Args… args

打印val值,剩下的参数继续以参数包的形式传给ShowList。

  • 传递实参(‘A’, “sort”),ShowList接收参数。
  • ‘A’ → T val
  • (“sort”) → Args… args

打印val值,剩下的参数继续以参数包的形式传给ShowList。

  • 传递实参(“sort”),ShowList接收参数。
  • “sort” → T val
  • 此时参数包中没有参数了,调用无形参的ShowList()。

打印val值,再调用不需要接收形参的ShowList(),打印换行,到此参数包的展开就完毕了。

这种方式很像递归,在函数模板中调用函数模板,通过模板参数中的第一个模板参数一个个从参数包中拿参数。不需要的形参的函数就相当于一个结束条件。

逗号表达式展开参数包:

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第17张图片

先上代码,后面再解释为什么这样。
【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第18张图片
同样将参数包挨个展开了。

  • 逗号表达式的结果是最右边的值。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第19张图片

  • 调用ShowList的时候,传入了四个参数。
  • 将参数包放在数组中,并且调用打印函数PrintArg。
{(PrintArg(args), 0)...};

展开成为:

{(PrintArg(arg1), 0), (PrintArg(arg2), 0), (PrintArg(arg3), 0), (PrintArg(arg4), 0)};

由于使用的是逗号表达式,所以(PrintArg(arg1), 0)会先调用PrintArg(arg1),最终返回最右边的值,也就是0。

参数包中的每个参数都执行这样的逻辑,最终返回的都是0,而这由0组成的列表又初始化了数组arr。

所以我们可以在调试窗口中可以看到数组中的值是0。

在这里,数组的目的仅仅是为了在构造数组的过程中展开参数包,它起到一个辅助作用

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第20张图片
采用上图所示方式也可以展开参数包。

  • 在ShowList中的数组中多次调用PrintArg函数,每次调用后返回值是0。
  • 多个0形参的列表初始化数组。

这种方式中,看起来比逗号表达式好理解,数组同样仅起辅助作用。

STL中emplace相关接口

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第21张图片
emplace的作用和insert类似。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第22张图片
emplace_back的作用和push_back相似。

  • C++11提供了emplace相关的系列接口,上图是以vector为例,其他STL容器也有emplace系列的相关接口。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第23张图片
emplace接口也是模板函数,它既是一个万能引用模板也是一个可变参数模板,可以称为万能引用可变参数模板。

  • 无论插入的数据是左值还是右值,无论是多少个,都可插入。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第24张图片

  • 对于内置类型,push_back和emplace_back没有任何区别。
  • 而且也不可以一次性插入多个内置类型的值。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第25张图片
只有对容器实例化后,并且存放多个值时,才能使用empalce_back一次性插入。如上图所示。

emplace相关接口的优势:

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第26张图片

将上篇文章C++11——新特性 | 右值引用 | 完美转发中的string做改造。

  • 拷贝构造改用传统写法,并且在用字符串的构造的构造函数中打印提示信息。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第27张图片
使用push_back插入不同类型的值。

  • 插入左值:调用拷贝构造函数,发生了深拷贝。
  • 插入右值(匿名键值对):调用构造函数和移动构造函数。
  • 插入右值(匿名initializer_lis对象):调用构造函数和移动构造函数。

其中调用构造函数都是在初始化"sort"时候调用的。插入左值编译器不敢进行资源转移,所以在new一个新节点的时候进行深拷贝,而插入右值时在new新节点时直接进行了资源转移。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第28张图片
使用emplace_back插入不同类型的值。

  • 插入左值:调用拷贝构造函数,发生了深拷贝。
  • 插入右值(匿名键值对):仅调用构造函数,相比于push_back,少调用了移动构造函数。
  • 插入多个值(可变参数):仅调用构造函数。

对比发现:

  • 插入左值时,emplace_back和push_back没有区别。

因为左值无论是编译器还是emplace_back都是不敢进行优化的,只能老老实实进行深拷贝,以防影响到原本的左值。

  • 插入右值(匿名键值对)时,emplace_back仅调用了构造函数。

在插入的过程中,匿名对象一直存在,没有被转移资源,知道链表在new一个新节点的时候,才用右值对象中的数据来初始化节点,其中string调用的是普通构造函数,是用右值中的字符串来初始化的。

  • 插入多个值(可变参数)时,emplace_back仅调用了构造函数。

和插入右值一样,只有在new一个新节点的时候,多个插入的值才被用来初始化,所以也是只调用了普通构造函数。

  • 只有在插入自定义类型的右值时,emplace_back的效率才比push_back高。
  • emplace_back比push_back少调用了一个移动构造函数。

我们知道,移动构造是将右值的资源进行转移,也是非常高效的,代价非常小。

emplace系列接口在存在移动构造的情况下,并不能比push_back高效很多,但还是高一点的。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第29张图片
将string中的移动构造函数屏蔽后。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第30张图片
使用push_back插入左值和右值。

  • 此时,无论是插入右值还是左值,调用的都是拷贝构造函数,进行了深拷贝。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第31张图片
使用emplace_back插入左值和右值。

  • 对于左值,仍然需要深拷贝。
  • 对于右值,则仅调用了构造函数,不用进行拷贝构造而发生深拷贝。

emplace_back相比于push_back少调用了拷贝构造,没有进行深拷贝,大大提高了效率,降低了系统开销。

对于不存在移动构造的情况下,emplace相关接口比push_back高效很多。

包装器

function

  • function包装器:也叫作适配器。

但是它和适配器又不一样,在模拟实现栈和队列的时候就是使用的适配器模式,适配器是在已有结构的基础上进行改造,如将正向迭代器改成反向迭代器。

而function包装器仅仅是进行包装,而不进行改造。它是一个类模板:

template <class Ret, class ...Args>
class function<Ret(Args...)>;
  • 模板参数:Ret,表示返回值类型,…Args是参数包,表示可接收的形参。
  • class function有点像模板特化的味道,Ret(Args…)其实就是函数去掉函数名,如int(char ch, int x)这样。

可以看出,function类包装的是有返回值和形参的可调用对象:

  • 函数指针
  • 仿函数对象
  • lambda表达式

在使用function包装器的时候,必须包头文件:

#include 

包装函数指针:

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第32张图片
实现一个函数func,进行两个数相加,返回值是int类型,两个形参也是int类型。

  • 在function实例化时使用,表示包装的可调用对象返回值和形参都是int类型。
  • function实例化对象时,可以使用拷贝构造方式初始化。
  • 也可以创建对象后再赋值。

包装过后,函数指针func就被包装成了f1,调用f1就可以执行函数func的逻辑,如上图结果所示。

包装仿函数对象:

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第33张图片

实现一个仿函数Functor,其中operator()的返回值是int,两个形参也是int。

  • 包装仿函数对象时,可以使用仿函数对象的左值初始化也可以使用右值初始化。
  • 和包装函数指针一样,可以使用调用拷贝构造的方式初始化,也可以使用赋值的方式初始化。

包装过后,仿函数对象就成了f2了,调用f2就可以执行仿函数对象的逻辑,如上图运行结果所示。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第34张图片
但是不能使用上图红色框中的方式来初始化function对象,即使是左值也不行。

  • 根据这里的报错可以得出看到,编译器在这里将Functor识别成了一个指针,并不是仿函数对象。

本喵觉得这里是VS2019的编译器的BUG,按道理是可以的,有兴趣的小伙伴可以试试其他编译器。

包装lambda表达式:

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第35张图片

  • 包装lambda表达式时,lambda可以是匿名对象,也可以是左值。
  • 可以使用拷贝构造的方式初始化,也可以赋值。

包装过后,lambda表达式就成f3了,调用f3就可以执行仿函数对象的逻辑,如上图运行结果所示。

包装静态类成员函数:

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第36张图片

  • 静态成员函数没有this指针,和普通函数的区别在于它处于某个类的类域中。
  • 类名::静态成员函数名就是一个函数指针,所以可以之间包装。
  • 对静态成员函数名的取地址符合可加可不加,建议加上。

对静态类成员函数包装后就成了f4了,调用f4执行的就是静态成员函数的逻辑,调用结果如上图所示。

包装类普通成员函数:

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第37张图片

  • 普通成员函数的调用是需要this指针的,而且this指针是不能显式传递的,只能由编译器来完成传递。
  • &类::成函数名拿到了成员函数的函数指针,这里必须有取地址符号
  • 包装器实例化时,除了成员函数的两个形参类型外,还需要有当前类的类型。
  • 在调用包装器时,除了传入的两个double类型的实参外,还要有当前类对象。

切记:实例化时需要的是当前类的类型,而不是当前类型的指针(this指针不会显式传递),调用包装器时,还需要传入当前类的对象。

对普通类成员函数包装后就成了f5了,调用f5执行的就是普通类成员函数的逻辑,如上图调用结果所示。

function作用之一:统一类型

函数指针,仿函数对象,lambda表达式,静态类成员函数,普通类成员函数,这些可调用对象是完全不相同的类型,但是使用function包装器包装以后,就都变成了function类型了。


【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第38张图片

创建一个函数模板,在模板函数中创建一个静态变量,打印它的值和地址,并且每打印一次后对其进行加加。通过实例化后的可调用对象执行相应的逻辑。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第39张图片

  • 使用函数f1实例化函数模板,并且给定的实参是1.5。
  • 使用仿函数对象实例化函数模板,给定的实参是1.5。
  • 使用lambda表达式实例化函数模板,给定的实参是1.5。

当然还可以用静态成员函数以及非静态成员函数实例化函数模板,有兴趣的小伙伴自己尝试。

  • 运行结果中有三个不同的静态变量(它们的地址不相同),而且值都是0。
  • 说明实例化出了3个函数。

模板实例化后推演为具体类型的工作是由编译器完成的,这其实有很大的系统开销,只是我们没有感觉罢了。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第40张图片

使用包装器将上面用来实例化函数模板的三种可调用对象进行包装,此时就都变成了function类型了,只有一种类型。

再用这种function类型去实例化函数模板,编译器推演后只实例化出一个函数。

  • 静态变量的地址都相同,说明它们是同一个静态变量。
  • 静态变量从0变化到2,说明这个函数被调用了3次。

经过function的包装后,减少了实例化的系统开销。

function作用之二:减少因多次实例化导致的系统开销

作用一和二的本质都是通过统一类型完成的,在需要将多种可调用对象统一类型的场景时,就使用function包装器。

bind

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第41张图片

  • bind:也叫做绑定,是一个函数模板,它就像一个函数包装器(适配器),接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表。
  • 这是一个万能引用模板,除了可调用对象模板参数外,其他参数是可变参数,也就是一个参数包。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第42张图片

bind(可调用对象, 占位1, 占位2.....);

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第43张图片
C++11提供了一个命名空间placeholders,该空间中的_1, _2, _3…表示占位符。

  • bind可调用对象时,并没有传参数,但是要根据可调用对象的形参个数先占好位。

上面代码表示:绑定函数plus参数分别由调用func1的第一、二个参数指定。

  • 可调用对象经过bind绑定以后,就称为绑定函数。

bind的作用主要有两个:调整可调用对象的参数顺序,绑定固定参数。

调整参数的顺序:

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第44张图片
创建一个作减法的仿函数,将仿函数对象绑定后再使用function包装。

  • func2和func3绑定的都是仿函数对象Sub()。
  • func2绑定仿函数对象时,占位的顺序是placeholders::_1, placeholders::_2。

在调用func2(1, 2)的时候,1→placeholders::_1(形参int a),2→placeholders::_2(形参int b),所以执行a-b时就是1-2=-1。

  • func3绑定仿函数对象时,占位的顺序是placeholders::_2, placeholders::_1。

在调用func3(1, 2)的时候,1→placeholders::_2(形参int b),2→placeholders::_1(形参int a),所以执行a-b时就是2-1=1。

根据这个道理,我们直接改变在使用sort()时的less仿函数比较方式。

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第45张图片
只是在绑定仿函数对象时,调整了参数的顺序,就可以让less仿函数实现greater的功能。

绑定固定参数:

【C++学习】C++11——lambda表达式 | 可变参数模板 | 包装器_第46张图片

在使用function统一可调用对象类型的时候,那些可调用对象中,只有类的普通成员函数包装后的类型和其他不一样,因为在包装时候需要多一个当前类的类型,调用时还需多一个当前类的对象。

  • 将类普通成员函数绑定时,将需要多的那个当前类对象的参数绑定。

如上图绿色框中所示,意味着将Sub()匿名对象绑定了,也就是编译器自动将这个参数在调用的时候传给类成员函数了。

我们在调用时只需要传递后两个参数即可。可以看到,两种方式的运行结果是一样的。

总结

lambda表达式是一个经常使用的东西,使用起来也很方便,非常时候临时使用一下。至于可变参数模板,只需要了解有这个东西就可以,知道emplace相关接口是可变参数模板,一次可以传入多个参数,我们自己几乎不会写这样的模板。function的时候也非常普遍,尤其在网络部分,至于bind只需要了解即可。

你可能感兴趣的:(C++学习,c++,学习)