引言:
前面我们学习了一些C++的主要知识,但是还有一些C++ 11 的知识值得我们来学习一下,因此这次我们就来学习一下C++ 11 中的一些重要知识。
一:列表初始化
1. C++98 中传统的{}
在C++98中,数组和结构体都支持使用{}来初始化。
2. C++ 11 中的{}
- C++11 中可以说是万物皆可用
{}
来初始化。 - 并且在初始化时可以省略掉
=
(1) 内置类型用{}初始化:
(2)自定义类型用{}
初始化:
**注:只有{}
初始化才支持省略 =
**
(3)容器插入数据时三种传参方式的对比
3. C++ 11 中的std::initializer_list
- 上面的初始化已经很方便,但是对象容器初始化还是不太方便,比如一个vector对象,我想用N个值去构造初始化,那么我们得实现很多个构造函数才能支持。
- C++11库中提出了一个
std::initializer_list
的类, 这个类的本质是底层开一个数组,将数据拷贝过来,std::initializer_list
内部有两个指针分别指向数组的开始和结束。 - 文档介绍:std::initializer_list,
std::initializer_list
支持迭代器遍历。- 容器支持一个
std::initializer_list
的构造函数,也就支持任意多个值构成的 {x1,x2,x3…} 进行初始化。STL中的容器支持任意多个值构成的 {x1,x2,x3…} 进行初始化,就是通过std::initializer_list
的构造函数支持的。
(1)std::initializer_list 存储位置探讨:
(2)std::initializer_list 的范围for遍历:
(3)std::initializer_list 给容器带来的便利:
二:右值引用和移动语义
- C++98的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性。
- C++11之后我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。
(1)左值和右值:
- 左值:左值是一个表示数据的表达式(如变量名或解引用的指针),一般是有持久状态,存储在内存中,我们可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时
const
修饰符后的左值,不能给他赋值,但是可以取它的地址。 - 右值:右值也是一个表示数据的表达式,要么是字面值常量、要么是表达式求值过程中创建的临时对象等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
- 左值和右值最关键的区别是能否取地址。
(2)左值引用和右值引用:
Type& r1 = x; Type&& rr1 = y;
第一个语句就是左值引用,左值引用就是给左值取别名,第二个就是右值引用,同样的道理,右值引用就是给右值取别名。- 左值引用不能直接引用右值,但是
const
左值引用可以引用右值。 - 右值引用不能直接引用左值,但是右值引用可以引用move(左值)。
template <class T> typename remove_reference<T>::type&& move (T&&arg);
move
是库里面的一个函数模板,本质内部是进行强制类型转换,当然他还涉及一些引用折叠的知识,这个我们后面会详细学习。- 需要注意的是变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量变量表达式的属性是左值。(这个稍后面一点再理解)
- 语法层面看,左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看下面代码中r1和rr1汇编层实现,底层都是用指针实现的,没什么区别。底层汇编等实现和上层语法表达的意义有时是背离的,所以不要然到一起去理解,互相佐证,这样反而是陷入迷途。(稍后面一点再理解)
(1) 左值引用:
(2)右值引用:
(3)const
左值引用 :
(4)move
:
(3)左值和右值的参数匹配:
- C++98中,我们实现一个
const
左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。 - C++11以后,分别重载左值引用、
const
左值引用、右值引用作为形参的f函数,那么实参是左值会匹配f(左值引用),实参是const
左值会匹配f(const
左值引用),实参是右值会匹配f(右值引用)。 - 右值引用变量在用于表达式时属性是左值,这个设计这里会感觉很怪,后面我们讲右值引用的使用场景时,就能体会这样设计的价值了。
一般来说,对于左值和右值的匹配,是有更合适的就会匹配更合适的,否则就匹配能匹配上的。比如这里对于右值引用的话const 左值引用和右值引用都可以匹配上,但是会更倾向于更合适的。
(4) 右值引用和移动语义的使用场景:
a. 左值主要使用场景回顾:
左值引用主要使用场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。左值引用已经解决大多数场景的拷贝效率问题,但是有些场景不能使用传左值引用返回,如下面的addStrings
和generate
函数,C++98中的解决方案只能是被迫使用输出型参数解决。那么C++11以后这里可以使用右值引用做返回值解决吗 显然是不可能的,因为这里的本质是返回对象是一个局部对象,函数结束这个对象就析构销毁了,右值引用返回也无法概念对象已经析构销毁的事实。
场景一:
这种情况下左值返回是不行的,因为这里是临时对象,右值返回也不行,因为这个临时对象出了函数栈帧就会销毁。
场景二:
这种情况下,只能用传值返回,因此这里会有很大的拷贝代价。
换到之前我们的解决方式就是改为输出型,就是提前创建数组,接着将数组以参数的形式传入。
b. 移动构造和移动赋值:
- 移动构造函数是一种构造函数,类似拷贝构造函数,移动构造函数要求第一个参数是该类类型的引用,但是不同的是要求这个参数是右值引用,如果还有其他参数,额外的参数必须有缺省值。
- 移动赋值是一个赋值运算符的重载,他跟拷贝赋值构成函数重载,类似拷贝赋值函数,移动赋值函数要求第⼀个参数是该类类型的引用,但是不同的是要求这个参数是右值引用。
- 对于像
string/vector
这样的深拷贝的类或者包含深拷贝的成员变量的类,移动构造和移动赋值才有意义,因为移动构造和移动赋值的第⼀个参数都是右值引用的类型,他的本质是要“窃取”引用的右值对象的资源,而不是像拷贝构造和拷贝赋值那样去拷贝资源,从提高效率。下面的bit::string
样例实现了移动构造和移动赋值,我们需要结合场景理解。
(1)约定:
为了更好地学习移动构造和移动赋值,下面我们拿我们自己实现的string
类来辅助理解。
(2)移动构造和移动赋值:
由于面对的是临时对象,因此需要用右值引用,但如果直接返回这个临时对象的引用的话会出问题,因为出了函数栈帧之后这个临时对象就会被销毁,这时候再访问的话就会出问题(野引用),因此就有了移动构造和移动赋值,反正你这个临时对象马上也要销毁了,你不如把你的数据直接给我,然后你再拿着我的数据去销毁,这就避免了拷贝,又解决了直接引用返回的问题。
(3)为什么右值引用变量属性为左值?
在了解完移动构造和移动赋值之后大家应该就能理解为什么要这样了,因为移动构造和移动赋值中要交换临时对象的数据,但如果其作为右值是无法进行修改的,因此这里将其右值引用的属性变为了左值,因此这里才能实现数据的交换(修改)
c. 右值引用和移动语义解决传值返回问题:
(1)场景一:
1. 右值对象构造,只有拷贝构造,没有移动构造的场景
- 下图展示了VS2019 debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次拷贝构造,右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次拷贝构造。
- linux下可以将下面代码拷贝到test.cpp文件,编译时用 g++ test.cpp -fno-elide-
constructors 的方式关闭构造优化,运行结果可以看到下图左边没有优化的两次拷贝。
需要注意的是在VS2019的release和vs2022的debug和release,下面代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造。变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解,如下图所示:
2. 右值对象构造,有拷贝构造,也有移动构造的场景:
- 下图展示了VS2019 debug环境下编译器对拷贝的优化,左边为不优化的情况下,两次移动构造,右边为编译器优化的场景下连续步骤中的拷贝合二为一变为一次移动构造。
- linux下可以将下面代码拷贝到test.cpp文件,编译时用g++ test.cpp -fno-elide-
constructors 的方式关闭构造优化,运行结果可以看到下图左边没有优化的两次移动。
需要注意的是在VS2019的release和vs2022的debug和release,下面代码优化为非常恐怖,会直接将str对象的构造,str拷贝构造临时对象,临时对象拷贝构造ret对象,合三为一,变为直接构造。要理解这个优化要结合局部对象生命周期和栈帧的角度理解,如下图所示:
3. 右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
- 下图左边展示了VS2019 debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境下编译器的处理,一次拷贝构造,一次拷贝赋值。
- 需要注意的是在VS2019的release和vs2022的debug和release,下面代码会进⼀步优化,直接构造要返回的临时对象,str本质是临时对象的引用,底层角度用指针实现。运行结果的角度,我们可以看到str的析构是在赋值以后,说明str就是临时对象的别名。
4. 右值对象赋值,既有拷贝构造和拷贝赋值,也有移动构造和移动赋值的场景
下图左边展示了VS2019 debug和 g++ test.cpp -fno-elide-constructors 关闭优化环境
下编译器的处理,一次移动构造,一次移动赋值。
d. 右值引用和移动语义在传参中的提效:
- 查看STL文档我们发现C++11以后容器的
push
和insert
系列的接口否增加的右值引用版本。 - 当实参是一个左值时,容器内部继续调用拷贝构造进行拷贝,将对象拷贝到容器空间中的对象。
- 当实参是一个右值,容器内部则调用移动构造,右值对象的资源到容器空间的对象上。
- 把我们之前模拟实现的
crow::list
拷贝过来,支持右值引用参数版本的push_back
和insert
。 - 其实这里还有一个
emplace
系列的接口,但是这个涉及可变参数模板,我们需要把可变参数模板学习以后再学习emplace
系列的接口。
这里可以看到确实是发生了移动构造,s1的数据被转移走了。
(5)引用延长生命周期:
右值引用可用于为临时对象延长生命周期,const
的左值引用也能延长临时对象生存期,但这些对象无法被修改。
(6)类型分类(了解):
- C++11以后,进一步对类型进行了划分,右值被划分纯右值(pure value,简称prvalue)和将亡值(expiring value,简称xvalue)。
- 纯右值是指那些字面值常量或求值结果相当于字面值或是一个不具名的临时对象。如: 42、true、nullptr 或者类似 str.substr(1, 2)、str1 + str2 传值返回函数调用,或者整形 a、b,a++,a+b 等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于C++98中的右值。
- 将亡值是指返回右值引用的函数的调用表达式和转换为右值引用的转换函数的调用表达,如move(x)、static_cast<X&&>(x)
- 泛左值(generalized value,简称glvalue),泛左值包含将亡值和左值。
- 值类别cppreference.com和Value categories这两个关于值类型的中文和英文的官方文档,有兴趣可以了解细节。
(7) 引用折叠:
- C++中不能直接定义引用的引用如
int& && r = i;
,这样写会直接报错,通过模板或?typedef
中的类型操作可以构成引用的引用。 - 通过模板或
typedef
中的类型操作可以构成引用的引用时,这时C++11给出了一个引用折叠的规则:右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用。 - 下面的程序中很好地展示了模板和
typedef
时构成引用的引用时的引用折叠规则,大家需要一个一个仔细理解一下。 - 像f2这样的函数模板中,
T&& x
参数看起来是右值引用参数,但是由于引用折叠的规则,他传递左值时就是左值引用,传递右值时就是右值引用,有些地方也把这种函数模板的参数叫做万能引用。 Function(T&& t)
函数模板程序中,假设实参是int右值,模板参数T的推导int,实参是int左值,模板参数T的推导int&,再结合引用折叠规则,就实现了实参是左值,实例化出左值引用版本形参的Function,实参是右值,实例化出右值引用版本形参的Function。
a. typedef 实现的引用折叠:
b. 左值引用模版:
c. 万能引用模版:
d. 基于万能引用模版下的探讨:
1. 无折叠:
该情况下编译器通过右值10推导出T的类型为int,不存在引用折叠,因此模版实例化的是右值引用的模版,这里我们还打印了下 a 和 x 的地址来验证了一下我们推理的正确性。
2. 左值引用:
该情况下编译器通过左值a 推导出T的类型为 int& ,因此模版实例化出的是左值引用的模版,这里我们也通过打印 x 和 a 的地址来验证了两者地址相同,因此我们的推理正确。
3. 右值引用:
该情况下编译器通过右值a推导出T为int ,未存在引用折叠,因此模版实例化出的是右值引用的模版,通过打印 x 和 a 的地址 验证了我们推理的正确性。
4.const 左值引用: 
这里由于b是左值,因此编译器推导出T为const int& ,,通过引用折叠之后模版实例化出的是const 左值引用,由于T 是 const int& 类型的,所以这里对x++就会出错。
5. const 右值引用:
该情况下,由于b为右值,因此编译器推导出T 为 const int 类型,不存在引用折叠,所以模版实例化出的为const 右值引用,并且由于T 为 const int 类型,因此这里的x就
不能进行++。
(8)完美转发:
Function(T&& t)
函数模板程序中,传左值实例化以后是左值引用的Function
函数,传右值实例化以后是右值引用的Function
函数。- 但是结合我们在前面章节的学习,变量表达式都是左值属性,也就意味着一个右值被右值引用绑定后,右值引用变量表达式的属性是左值,也就是说
Function
函数中t的属性是左值,那么我们把t传递给下一层函数Fun,那么匹配的都是左值引用版本的Fun函数。这里我们想要保持t对象的属性,就需要使用完美转发实现。 template <class T> T&& forward (typename remove_reference<T>::type&arg);
template <class T> T&& forward (typenameremove_reference<T>::type&& arg);
- 完美转发
forward
本质是一个函数模板,他主要还是通过引用折叠的方式实现,下面、示例中传递给Function
的实参是右值,T被推导为int,没有折叠,forward
内部t被强转为右值引用返回;传递给Function
的实参是左值,T被推导为int&,引用折叠为左值引用,forward
内部t被强转为左值引用返回。
初始代码:
通过上面的案例我们可以看到打印出的结果都是左值引用,这是为什么呢?
当右值被引用后,t的属性是左值,再调用Fun的话就都是左值引用了。
forward改进:
三:可变参数模版(了解)
1. 基本语法及原理:
- C++11支持可变参数模板,也就是说支持可变数量参数的函数模板和类模板,可变数目的参数被称为参数包,存在两种参数包:模板参数包,表示零或多个模板参数;函数参数包:表示零或多个函数参数。
template <class ...Args> void Func(Args... args) {}
template <class ...Args> void Func(Args&... args) {}
template <class ...Args> void Func(Args&&... args) {}
- 我们用省略号来指出一个模板参数或函数参数的表示一个包,在模板参数列表中,
class...
或typename...
指出接下来的参数表示零或多个类型列表;在函数参数列表中,类型名后面跟...
指出接下来表示零或多个形参对象列表;函数参数包可以用左值引用或右值引用表示,跟前面普通模板一样,每个参数实例化时遵循引用折叠规则。 - 可变参数模板的原理跟模板类似,本质还是去实例化对应类型和个数的多个函数。
- 这里我们可以使用
sizeof...
运算符去计算参数包中参数的个数。
a. 计算参数包中的参数个数:
b. 原理:
2. 包扩展:
- 对于一个参数包,我们除了能计算他的参数个数,我们能做的唯一的事情就是扩展它,当扩展一个包时,我们还要提供用于每个扩展元素的模式,扩展一个包就是将它分解为构成的元素,对每个元素应用模式,获得扩展后的列表。我们通过在模式的右边放一个省略号(…)来触发扩展操作。底层的实现细节如下图所示。
- C++还支持更复杂的包扩展,直接将参数包依次展开依次作为实参给一个函数去处理。
是不是有点俄罗斯套娃的味呢?
a. 错误用法场景:运行时解析
因为参数包本质是搞了一个数组将参数都存放起来了,但是没有一个容器能够存放不
同类型的数据,而且可变参数模版是在编译时解析,因此这种写法就是有问题的。
3. empalce
系列接口:
template <class... Args> void emplace_back (Args&&... args);
template <class... Args> iterator emplace (const_iterator position, Args&&... args);
(1) emplace_back 与 push_back 效率对比(传入单一类型):
可以看到在传入左值和右值的情况下两者的没啥区别,但是在直接传入实参的参数时就有所区别了,前者为构造+移动构造+析构,但是后者是直接构造。
这是为什么呢?
(2) emplace_back 与 push_back 效率对比(传入pair类型):
可以这里看到这里的效果和上面的效果基本上一样。
注:注意这里在传入pair类型的时候是emplace_back是不支持花括号形式来传参的,
因为花括号牵扯到std::initializer_list
,但是这里面的参数都是同一类型的,因此这样写是不对的。
小结:
emplace_back
在容器插入时的传参是构造T对象的参数包,直接构造效率更高,因此在STL容器中,我们更推荐使用emplace_back
系列,但是要注意使用方法。
四:C++11中新的类功能
1. 默认的移动构造和移动赋值:
- 原来C++类中,有6个默认成员函数:构造函数/析构函数/拷贝构造函数/拷贝赋值重载/取地址重载/
const
取地址重载,最后重要的是前4个,后两个用处不大,默认成员函数就是我们不写编译器会生成一个默认的。C++11新增了两个默认成员函数,移动构造函数和移动赋值运算符重载。 - 如果你没有自己实现移动构造函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
- 如果你没有自己实现移动赋值重载函数,且没有实现析构函数、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)
- 如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
a. 编译器生成了默认移动构造和默认移动赋值:
b. 编译器未实现默认移动构造和默认移动赋值:
2. 成员变量声明时给缺省值:
这部分在类和对象(下)讲解过,不清楚的可以看我的这篇博客:类和对象(下)
3. default
和 delete
- C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用
default
关键字显示指定移动构造生成。 - 如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明不实现,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上
=delete
即可,该语法指示编译器不生成对应函数的默认版本,称=delete
修饰的函数为删除函数。
(1) default
强制编译器生成默认函数:
此时因为我们实现了拷贝构造,因此编译器就没有生成默认移动构造函数
用default
强制编译器生成默认移动构造函数:
(2) delete
指示删除函数:
4. final
与override
这两个关键字之前在继承和多态这两节讲解过,可以参考这两篇博客:
类和对象—继承
类和对象—多态
五:STL中的一些变化
- 下图中圈起来的就是STL中的新容器,但是实际最有用的是
unordered_map
和unordered_set
。这两个我们前面已经学习过了,其他的大家了解一下即可。 - STL中容器的新接口也不少,最重要的就是右值引用和移动语义相关的
push/insert/emplace
系列接口和移动构造和移动赋值,还有initializer_list
版本的构造等,这些前面都讲过了,还有一些无关痛痒的如cbegin/cend
等需要时查查文档即可。 - 容器的范围
for
遍历,这个在容器部分也讲过了。
六:lambda
1. lambda 表达式语法:
lambda
表达式本质是一个匿名函数对象,跟普通函数不同的是他可以定义在函数内部。lambda
表达式在语法使用层而言没有类型,所以我们一般是用auto
或者模板参数定义的对象去接收lambda
对象。lambda
表达式的格式:[capture-list] (parameters)-> return type {function boby }
[capture-list]
:捕捉列表,该列表总是出现在lambda
函数的开始位置,编译器根据[]来判断接下来的代码是否为lambda
函数,捕捉列表能够捕捉上下文中的变量供lambda
函数使用,捕捉列表可以传值和传引用捕捉,具体细节在后面我们再细讲。捕捉列表为空也不能省略。(parameters)
:参数列表,与普通函数的参数列表功能类似,如果不需要参数传递,则可以连同()
一起省略->return type
:返回值类型,用追踪返回类型形式声明函数的返回值类型,没有返回值时此部分可省略。一般返回值类型明确情况下,也可省略,由编译器对返回类型进行推导。{function boby}
:函数体,函数体内的实现跟普通函数完全类似,在该函数体内,除了可以使用其参数外,还可以使用所有捕获到的变量,函数体为空也不能省略。
lambda表达式举例:
2. 捕捉列表:
lambda
表达式中默认只能用lambda
函数体和参数中的变量,如果想用外层作用域中的变量就需要进行捕捉。- 第一种捕捉方式是在捕捉列表中显示的传值捕捉和传引用捕捉,捕捉的多个变量用逗号分割。
[x,y,&z]
表示x和y值捕捉,z引用捕捉。 - 第⼆种捕捉方式是在捕捉列表中隐式捕捉,我们在捕捉列表写一个
=
表示隐式值捕捉,在捕捉列表写⼀个&
表示隐式引用捕捉,这样我们lambda
表达式中用了那些变量,编译器就会自动捕捉那些变量。 - 第三种捕捉方式是在捕捉列表中混合使用隐式捕捉和显示捕捉。
[=,&x]
表示其他变量隐式值捕捉,x引用捕捉;[&,x,y]
表⽰其他变量引用捕捉,x和y值捕捉。当使用混合捕捉时,第一个元素必须是&
或=
,并且&
混合捕捉时,后面的捕捉变量必须是值捕捉,同理=
混合捕捉时,后面的捕捉变量必须是引用捕捉。 lambda
表达式如果在函数局部域中,他可以捕捉lambda
位置之前定义的变量,不能捕捉静态局部变量和全局变量,静态局部变量和全局变量也不需要捕捉,lambda
表达式中可以直接使用。这也意味着lambda
表达式如果定义在全局位置,捕捉列表必须为空。- 默认情况下,
lambda
捕捉列表是被const
修饰的,也就是说传值捕捉的过来的对象不能修改,mutable
加在参数列表的后面可以取消其常量性,也就说使用该修饰符后,传值捕捉的对象就可以修改了,但是修改还是形参对象,不会影响实参。使用该修饰符后,参数列表不可省略(即使参数为空)。
(1)值捕捉 / 传引用捕捉:
值捕捉的变量不能被修改。
传引用捕捉的变量可以改变。
(2)隐式值捕捉(用了哪些值就捕捉那些)
(3)隐式引用捕捉:
(4)混合捕捉1(主传引用,次传值)
这里默认都为传引用捕捉,但是a,b是传值捕捉。
(5)混合捕捉2(主传值,次传引用)
这里默认是传值捕捉,但是a,b是传引用捕捉,因此可以修改。
(6)局部变量和全局变量的捕捉:
(7)mutable修饰捕捉:
3. lambda
的应用:
- 在学习
lambda
表达式之前,我们的使用的可调用对象只有函数指针和仿函数对象,函数指针的类型定义起来比较麻烦,仿函数要定义一个类,相对会比较麻烦。使用lambda
去定义可调用对象,既简单又方便。 lambda
在很多其他地方用起来也很好用。比如线程中定义线程的执行函数逻辑,智能指针中定制删除器等,lambda
的应用还是很广泛的,以后我们会不断接触到的。
(1)之前的仿函数支持排序:
(2)lambda
支持的排序:
4. lambda
的原理:
lambda
的原理和范围for
很像,编译后从汇编指令层的角度看,压根就没有lambda
和范围for
这样的东西。范围for
底层是迭代器,而lambda
底层是仿函数对象,也就说我们写了一个lambda
以后,编译器会生成一个对应的仿函数的类。- 仿函数的类名是编译按一定规则生成的,保证不同的
lambda
生成的类名不同,lambda
参数/返回类型/函数体就是仿函数operator()
的参数/返回类型/函数体,lambda
的捕捉列表本质是生成的仿函数类的成员变量,也就是说捕捉列表的变量都是lambda
类构造函数的实参,当然隐式捕捉,编译器要看使用哪些就传那些对象。
七:包装器
1.function
std::function
是一个类模板,也是一个包装器。std::function
的实例对象可以包装存
储其他的可以调用对象,包括函数指针、仿函数、lambda
、bind
表达式等,存储的可调用对象被称为std::function
的 目标 。若std::function
不含目标,则称它为空 调用 空std::function
的 目标导致抛出std::bad_function_call
异常。- 以上是
function
的原型,他被定义<functional>
头文件中。std::function-cppreference.com是function
的官方文件链接。 - 函数指针、仿函数、
lambda
等可调用对象的类型各不相同,std::function
的优势就是统一类型,对他们都可以进行包装,这样在很多地方就方便声明可调用对象的类型。
(1)function
包装调用对象:
(2)function
包装静态成员函数:
(3) function
包装成员函数:
(4)function
解题应用:
1. 题目:
2. 传统解题方法:
3. function
优化:
2. bind
bind
是一个函数模板,它也是一个可调用对象的包装器,可以把他看做一个函数适配器,对接收的fn
可调用对象进行处理后返回一个可调用对象。bind
可以用来调整参数个数和参数顺序。bind
也在<functional>
这个头文件中。- 调用
bind
的一般形式:auto newCallable = bind(callable,arg_list);
其中
newCallable
本身是一个可调用对象,arg_list
是一个逗号分隔的参数列表,对应给定的callable
的参数。当我们调用newCallable
时,newCallable
会调用callable
,并传给它arg_list
中参数。 arg_list
中的参数可能包含形如_n
的名字,其中n
是⼀个整数,这些参数是占位符,表示newCallable
的参数,它们占据了传递给newCallable
的参数的位置。数值n表示生成的可调用对象中参数的位置:_1
为newCallable
的第一个参数,_2
为第二个参数,以此类推。_1/_2/_3
…这些占位符放到placeholders
的⼀个命名空间中。
(1)调整参数顺序: