1.lambda
lamba表达式本质是匿名函数
书写格式:[ 捕捉列表 ] + ( 参数 ) + mutable + -> 返回值 + { 函数体 }
下面我会由简到难分享lambda的用法和它的理解
(1)基本使用和理解
我们先来看一段代码
我认为第一次看这个表达式应该还是能够看懂,但是里面的语法细节需要一条一条解释
这个时候疑问就更多了,为什么只能用auto?哪里体现出匿名了,fun不是有名字吗?
现在可以解释的是lambda表达式本质上是一个类,而这个类的名字我们是不知道的,auto fun = 这个操作其实是一种拷贝构造,我们也可以理解为给这个类起了一个新名字,后续要使用这个类就得用这个新名字去操作,功能和原来的类是一样的。
通过我们之前的学习便可猜出,fun(m, n)这种操作是调用仿函数的operator(),现在我们知道这一点就足够了,后续我们会不断深入。
(2)传值捕获
在使用lambda的函数体中我们可以使用传递过来的参数和全局域的对象(注意同名时优先局部变量,要避免同名),这点和函数是一样的,但lambda还可以使用捕捉列表的对象。捕捉列表捕捉的是当前域的其他变量,不能跨域
捕捉列表是异于函数的一种新用法,因为lambda表达式都是那种仅在当前域有作用,针对临时环境搭建的临时函数,所以捕捉列表让它能更好地在这种场景发挥优势,更好地利用该局部域的变量。并且捕捉列表是随着lambda写死的,不受传参影响。
捕捉和传参同样有两种方式,一种叫传值捕捉,一种叫传引用捕捉,上面演示的就是传值捕捉。
传值捕捉到的对象都会用const修饰,是不能修改的
如果要修改就必须加上mutable
如我演示那样,mutable加上以后我们就可以对捕获的对象修改值了,不过注意我们是传值捕获,和传值传参一样都是开辟了一块新的空间单独存的,所以lambda内部的修改不会影响实际被捕获对象的值(要慎重使用,因为名字都一样很容易搞混)。
(3)传引用捕获
在捕获列表中,只要变量前加上&都叫做传引用捕获,如&a,虽然&a一般还有取地址的含义,但在捕捉列表中还是很好区分的。
从上图中我们要注意细节,首先是不需要加mutable了,即传引用捕捉没有const修饰,因为引用的功能是取别名,不会修改名字的属性。这就意味着我们可以直接在匿名函数体内修改变量的值,并影响外面了,如图中的m受到了修改。
传值捕获和传引用捕获还有两个快捷操作:[=]所有值传值捕捉,[&]所有值引用捕捉。
也可以[=, &a](混合捕获)针对a传引用捕获,其余的传值捕获,随机搭配均可,可以帮我们灵活捕获。注意[=, &a]可行,[&a, =]不可行,顺序这点细节要抓住
需要注意的是[=]虽然在语法上是所有值传值捕捉,但在编译器层面只会传调用了的变量,编译器或尽可能优化掉不需要的性能开销,这一点我们表面感知不到
(4)lambda底层——类
前面我们已经铺垫过,lambda的底层是一个类。这个类的名字是编译器自动生成的,格式为:lambda_UUID唯一识别码。在编译之前,我们都不可能知道这个类的名字。当我们想要用ret来接收这个lambda表达式时,就只能用auto类型(返回的是一个类,类的名字不知道,只能用auto推导)。每个lambda都对应唯一的UUID唯一识别码,就算里面实现的内容一样,但UUID唯一识别码不会相同。
既然lambda底层是类,那么所谓的函数参数,捕捉列表等都有各自对应的类的语法。其中,fun(1, 2)是调用类的operator(),函数参数列表对应的是operator()的参数列表。捕捉列表对应的是成员变量,捕捉的本质是初始化成员变量。
(5)lambda的省略写法
lambda表达式捕捉列表和函数体任何情况下都不能省略,其余的在特定情况下都能省略。
返回类型可以自动推导,只要写了return都可以自动推导,可以不写;无参可以不写();mutable按需写
不写return均默认空返回,即void
2.function包装器(适配器)
auto ret = lambda表达式其实已经传递出了一种包装的思想。lambda类型复杂不确定,那我们就使用ret给它包装一层,如果后续要调用的话直接去找外层的包装ret即可。function进一步延续了这样的思想。
function包装的是一切能用函数的形式去调用的对象,包括函数指针(函数名),仿函数以及lambda。这三种对象都有很明显的函数的特征。
包装形式:function < void ( char, int ) > f1 = Fun;
(1)包装函数指针、lambda、仿函数
其中,void是函数的返回值,括号内的char和int是函数参数,f1是这个包装器的名字,赋值号=后面可以跟函数指针(函数名)、仿函数实例化出的对象名、lambda表达式(auto ret = lambda表达式中的ret,也可以直接跟lambda表达式一次性包装到位)。我们发现通过这种方式我们可以将所有返回值和参数相同的函数相关的对象分成一类
仿函数的使用需要稍微注意,首先赋值号后必须要跟实例化出的对象的名字,函数指针、lambda都是要实例化出对象才能包装;其次,operator()必须是非static的public成员
反之在不知道具体函数的情况下也可以先用包装器占位
我们发现包装器和函数指针有相似之处,在C语言中就常用函数指针来实现包装。但C语言中函数出现的形式有限,在C++中仿函数、lambda的出现使得函数指针过于局限,如此包装器就是为解决这个问题出现的。
map<string, function<int(int, int)>> m就可以将key和对应的操作联系起来,在很多游戏中很实用。
(2)包装类的静态成员函数
静态成员函数也能包装,要指定类域,可以不加&,注意要包装静态成员函数要保证该成员函数是在public修饰下
由于类的非静态成员函数是在类的公共区域,所以直接指定类域即可
(3)包装类的非静态成员函数
这可以说是包装最难的一部分了,因为类的非静态成员函数是C++中函数最特殊的存在,第一个参数是this让包装变得很棘手。
第一种解决办法就是补上这个隐藏的参数
有一个小点要注意的是传&A::add,非静态成员函数必须加&,而且由于只是包装add这个函数指针,所以直接指定类域即可,a.add(1, 2)只能调用函数,没有传递指针的功能
但是如果仔细想,这里有个问题,如果f1(&a, 1, 2)的括号内的内容会直接去调用add函数的话,一定会报错,因为this不能显式传参。但为什么能跑通呢?
注意,function对类的非静态成员函数有特殊的处理。其它的像lambda、函数指针、类的静态成员函数都是直接调用对应函数的operator(),而非静态成员函数不是这样。
首先,对于非静态成员函数,会先调用function的operator(),function会识别第一个参数,这里传的是指针A*,实例化的对象是&a,将这个指针作为function的成员变量存起来。之后function会类似调用&a -> &A::add(1, 2),参数是除开第一个参数的剩余参数,如此就能顺利调用了。从这个过程中我们也可以意识到,类的非静态成员函数的调用必须要先实例化对象,不然function无法处理。
从这个原理中,我们发现,关键的点在于function中途处理了一次,利用我们所传的第一个参数来调用赋值号后的函数指针。function会自动判断调用方式。由此一来,另外一种处理方式就不难理解了。
同理,function会用第一个参数A()使用.的方式调用&A::add,参数是从我们所传参数的第二个算起。调用方式和类型判断是function的事,我们不用关心。
3.bind
bind绑定是又一种包装,它同样作用于函数形式的对象。前面的function虽然在一定程度上包装了函数,但像非静态成员函数的包装依然不完美,它并不能消除this带来的影响。并且在函数参数的顺序上,有可能我们想要做出统一,但function也不支持,bind就能解决这个问题。
(1)命名空间placeholders
在<functional>里,有一个命名空间里声明的变量需要我们认识
从_1到_20,这些都被称为占位符
当使用using placeholders::_1;时,_1会被引入该全局域并定义,也就是说,_1是一个全局变量,它的生命周期贯穿整个程序。同理,当使用using namespace placeholders;时,placeholders里面20个占位符都会引入全局域并定义。
function是一个模板类,而bind是一个模板函数,但这个模板函数的返回值和lambda一样不可预见,但它可以直接被function接收,所以bind常和function连用
基本形式:bind( fun ( 可调用对象 ) , _1 , _2 )
这里我们可以看到,f1调用的顺序反过来了,这就是bind对传参顺序的调整。
我们从更复杂的情况来分析,这样更加全面
简单来说,调用处按顺序匹配占位符,bind处按顺序调用函数
在bind中,我们还可以将某个位置用具体值占领,这个时候function里面参数的个数要改,实际传参也要改,调用规则不变
如果添加了占位符,一定要修改参数个数和传参,不然多余的参数会被直接舍弃,遇上类型不匹配还要报错,情况太复杂,不建议深究,也完全没有意义。
在上面的基础之上,我们就可以先对函数进行第一步包装,再由function统一,其中最大受益者莫过于非静态函数的成员函数
通过bind,我们还可以直接绑定第一个参数为匿名对象A(),实际传参的过程是看bind后面的参数,而不是调用处和function处,因此虽然function的参数处没写,但A()是会传过去的,因此能够正常调用。