六、lambda表达式(叫表达式,其实是可调用对象)
1.lambda表达式的用法和本质
2.
那如果需要比较的性质特别多呢?比如要比较商品的名字,价格,评价等等,并且要实现从小到大和从大到小的仿函数,那我们就需要实现6个仿函数,这样岂不是太繁琐了,写6个struct类,如果类的命名不太好,比如按照1-6来命名类,那看代码的人每看到一个仿函数都需要向上去找对应的仿函数看看具体是什么功能,这样也太麻烦了吧!
C++此时觉得光有一个仿函数可调用对象有点不太够啊,能不能再搞出一个比仿函数用起来还舒服的对象呢?此时lambda表达式就登场了,lambda表达式的本质也是一个可调用对象,此时就无需再实现仿函数类什么的了,我们直接写一个lambda表达式给sort传过去,这样就可以一行代码搞定传可调用对象的问题了。
3.
lambda表达式书写格式:[capture-list] (parameters) mutable -> return-type { statement }
[capture-list] : 捕捉列表,该列表总是出现在lambda函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda函数,捕捉列表能够捕捉上下文中的变量供lambda函数使用。
(parameters): 参数列表。与普通函数的参数列表一致,如果不需要参数传递,则可以连同()一起省略
mutable: 默认情况下,lambda传值捕捉变量时,默认是const传值捕捉,mutable可以取消其常量
性。使用该修饰符时,参数列表不可省略(即使参数为空)。
->returntype: 返回值类型。用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。
{statement}: 函数体。在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量
4.
sort内部进行排序的时候,会依次向后两两比较vector的元素,在比较时就会用我们传的可调用对象进行比较,然后给可调用对象传两个vector元素过去,根据比较结果开始进行排序,所以lambda表达式和仿函数对象一样都是可调用对象,lambda表达式的参数也和仿函数类一样,都是Goods类对象的常引用。
5.
值得注意的是,lambda表达式的类型我们是写不出来的,这个类型是编译器自己生成的,所以这也就注定限制了我们使用lambda表达式的语法,像下面代码一样,我们只能用auto关键字自动推导lambda类型定义出compare对象,或者直接拿lambda这个匿名对象进行调用,只有这两种使用方式。
补充知识点: 使用宏的时候,换行时需要加续行符,因为宏必须是完整的一行,连空格都不能有。其他场景不需要加续行符。
6.
捕捉列表可以捕捉lambda外面的所有变量,但前提是这些变量都得在lambda表达式的上面。lambda的函数体除能够使用参数列表被别人传过来的值外,还可以使用捕捉列表里面所捕捉到的变量。
捕捉变量的方式有两种,分为传值捕捉和传引用捕捉,传值捕捉是const修饰的,所以如果想要修改传值捕捉的变量,则可以利用mutable来修饰,即取消传值捕捉变量的const属性。传引用捕捉并没有const修饰,可以直接修改,无需可变关键字。
捕捉列表中只有=时,代表传值捕捉lambda父作用域中lambda表达式向上的所有变量,如果有this指针则也可以捕捉this指针。
捕捉列表中只有&时,代表传引用捕捉lambda父作用域中lambda表达式向上的所有变量,如果有this指针则也可以捕捉this指针。
捕捉列表不允许变量重复传递,否则就会导致编译错误。 比如:[=, a]:=已经以值传递方式捕捉了所有变量,再次捕捉a变量重复。
7.
我们还需要了解一下lambda底层到底是什么,其实lambda底层就是仿函数对象,编译器自动生成了一个lambda的类,并在这个类里面实现了operator(),这个类是编译器自己生成的,每一个lambda的类都是不一样的,执行lambda函数体实际还是执行lambda类里面的operator(),所以本质上lambda和仿函数一样,只不过仿函数的类是我们自己写的,lambda的类是编译器自己随机生成的。
8.
C++允许使用一个lambda表达式拷贝构造一个新的副本,但不允许lambda表达式之间相互赋值。在了解lambda的底层之后,我们就可以理解了,因为各个lambda表达式的类都不一样,所以各个lambda表达式对象都没有关系,不能调用拷贝赋值函数。
但拷贝构造还是可以的,因为lambda还没初始化出来嘛,赋值是已经存在一个lambda对象了,拷贝构造就相当于搞出来一个lambda的副本,和原来的lambda共用编译器随机生成的同一个类。
void (*PF)(); int main() { auto f1 = []{cout << "hello world" << endl; }; auto f2 = []{cout << "hello world" << endl; }; //f1 = f2; // 编译失败--->提示找不到operator=() // 允许使用一个lambda表达式拷贝构造一个新的副本 auto f3(f2); f3(); // 可以将lambda表达式赋值给相同类型的函数指针,本质都是可调用对象嘛! PF = f2; PF(); return 0; }
2.配合多线程使用lambda表达式
1.
假设我们期望两个线程并发式的从0打印到99,我们可以选择实现两个函数,然后分别让线程并发的去运行,这样的方式其实就是给线程传函数指针,函数指针就是可调用对象嘛,线程刚好可以执行
2.
除上面那种方式外,我们其实还可以利用lambda表达式,在创建线程的同时传可调用对象lambda过去,lambda引用捕捉一下i就可以,要注意区分参数列表和捕捉列表,虽然捕捉列表看上去像是在传参,但实际并不是传参,仅仅是捕捉变量而已。
3.
下面的使用方式灵活的体现了C++面向对象的特性,我们将线程当作对象存储到容器vector里面,创建线程的同时将lambda可调用对象传给线程,这样所有的线程就会同时并发的打印0-99数字。
七、可变参数模板
1.展开参数包的两种方式(递归展开,借助数组推开参数包)
1.
C++新引入了可变参数模板的语法,即函数的参数可为一个参数包,这个参数包中可以包含任意个数的函数形参,想打印出参数包中参数的个数,可以通过sizeof…()函数取到参数包中参数的个数。
2.
下面是第一种展开参数包的方式,即递归方式调用ShowList,递归结束条件就是参数个数为0的ShowList()函数,在不断递归调用ShowList的过程中,参数个数会逐渐减少,直到args…的个数为0时,此时递归结束,调用无参的ShowList即可。
3.
下面是第二种展开参数包的方式,上面那种方式需要多增加一个模板参数T,用T定义出的val来表示单个的参数。
下面是通过辅助数组arr来实现推开参数包,在推的过程中调用PrintArg来打印出每个参数是什么,每推出来一个参数,就会调用对应的PrintArg函数进行参数的打印。
第一种屏蔽的方式就是逗号表达式,他会在推参数包的过程中顺便将arr数组初始化为0,但其实不初始化也没有关系,直接推参数包也行。
2.对比emplace和insert(使用语法 和 插入的效率)
1.
C++11新引入的emplace接口既有可变参数模板,又有万能引用,看起来很牛嘛,那他真的比insert接口效率高很多嘛?实际上并没有高很多,可能也就强那么一点点。
2.
在使用形式上,emplace支持直接传参数,不用自己构造键值对,调用像push_back和insert这样的接口时,需要先构造出键值对,然后调用移动构造版本的push_back函数进行键值对的插入,而emplace直接传pair键值对的俩参数就可以,emplace会直接用这个参数包构造出pair对象,并将对象插入到mylist里面。
所以在使用形式上emplace比push_back更加简洁一些,因为只需要传参数就可以。当然你如果也想构造键值对进行插入,emplace也是可以做到的。
3.
在效率上面两者的差距也不大,一个是直接构造,一个是先直接构造然后再移动构造。所以emplace也没有那么的牛,因为移动构造的代价也很低,只能说emplace比insert稍微强一点吧!(emplace对标insert,emplace_back对标push_back)
但如果string没有实现移动构造的话,那两者差距还是挺大的,一个是直接构造,一个是先直接构造然后再深拷贝。但我们不用担心这一点,下面代码是拿我们自己实现的string测试的,STL里面的容器哪个没有实现移动构造啊!所以这两个接口的效率差距也不大,甚至可以忽略不计。
下面是string实现了移动构造的场景
下面是string没有实现移动构造的场景
八、function包装器
1.对学过的所有可调用对象进行包装
1.
function就像范围for一样都是语法糖,看起来很牛逼,底层的实现并不复杂,function用起来还是非常香的,语法很简单,并且很好用,C++委员会总算干点儿正事了。
function学起来并不困难,他其实就是将我们原来所学的可调用对象,例如函数指针,仿函数对象,lambda进行包装,使其变成一个新的可调用对象,这个可调用对象就是包装器,有人说为什么要包装啊?以前的可调用对象用起来不是挺好的吗?你说的没错,但是包装过后,无论你是什么类型的可调用对象,在使用形式上统一都是包装器定义出来的对象的使用形式,在语法上更加的便捷
2.
对于下面函数模板useF来说,如果传函数指针,仿函数对象,lambda就会导致模板实例化出三份不同的函数实体来,导致模板的效率有些低。但如果我们将上面三个可调用对象进行包装,那就只会实例化出一份函数实体,但是却依靠这一份函数实体,实现了三种可调用对象的调用,不用像原来一样实例化出三份函数实体分别去调用函数指针,仿函数对象,lambda,这就是包装器带来的价值。
3.
事实上,你可以这么理解包装器,包装器也是一个仿函数对象,他的内部也实现了operator(),但他的operator()内部又调用了包装器包装的可调用对象的operator(),所以包装器这个类可以理解为他内部封装了三个可调用对象的operator(),在调用时根据不同的可调用对象,去调用包装器内部对应的operator()。
这里有点像多态,可调用对象是函数指针,那就调对应封装函数指针的包装器。可调用对象是函数对象,那就调对应封装函数对象的包装器。可调用对象是lambda,那就调对应封装lambda的包装器。
2.逆波兰表达式求解–包装器的使用
1.
像下面这样命令和动作对应的场景,其实就可以用包装器,让包装器包装lambda,然后把string和包装器对象构成的键值对存储到map里面,建立命令和动作的映射关系。
在调用对应的lambda时,我们就不用写一长串lambda然后加上(x,y)这样的调用方式了,而是直接用function包装器加上(x,y)这样的调用方式。
所以function用起来还是很香的。
3.bind绑定的用法
1.bind绑定其实和function是一种适配器模式,就像vector适配出stack,list适配出queue一样。
bind的用法也是花里胡哨的,下面列出了两种bind的用法。
一种是调整参数的顺序,通过调整占位对象来实现。
另一种是固定绑定参数,在绑定类成员函数时,function要在模板参数第一个位置加类名,在调用的时候也需要先传一个该成员函数所属类的对象(平常我们直接传匿名对象了就),这样用起来有点烦,所以可以在绑定类成员函数的同时,固定第一个参数为类的匿名对象,这样在使用包装器调用类成员函数的时候,就不需要再显示传一个匿名对象了。
2.
下面是绑定在控制参数时的用法,我们可以在绑定的同时给可调用对象显示传参数,也可以用占位对象_1 _2 _3…等等来替代参数位置,等待包装器调用的时候再传参数。