c++11 列表初始化 右值引用 移动语义 引用折叠 完美转发
C++11是C++的第⼆个主要版本 并且是从C++98起的最重要更新 引入了一些新的语法内容让C++ 代码更简洁、高效且易于维护 本文介绍了部分c++11的语法内容
列表初始化
这里的列表初始化和之前的构造函数那里的初始化列表名字上非常相似 但是他们没有一点关系
在c++98中用{}来初始化的是数组和结构体这两种 也是c语言的方式
在C++11以后想统⼀初始化方式,试图实现⼀切对象皆可用{}初始化,{}初始化也叫做列表初始化 {}初始化的过程中 可以省略掉=
----内置类型可以支持{}初始化
----自定义类型支持
这里的d1的创建实际上是先用后面{}中的内容调用了构造函数创建了一个临时对象 然后通过拷贝构造函数把临时内容拷贝给d1 不过经过编译器优化后变成了直接构造
d2是后面{}构造的临时对象的引用 会把临时对象的生命周期延长
如上的一些样例可以看到列表初始化使用起来非常的方便
initializer_list
initializer_list可以支持在构造的时候一下子插入很多的数据 如下面这种方式 其实就是先把{}里面的内容转换为initializer_list类型 然后进行了构造
底层就类似于先创建了一个数组 然后再一个一个进行了插入
在c++11中 容器增加了initialist_list的插入方式
{}外面可以不加()也可以加 不过这两种方式本质是不同的
①外层有() 会先把{}里面的内容转换为initialist_list类型 然后再调用构造函数
②外层没有() 是先构造了一个临时对象 然后进行拷贝构造 不过编译器优化后和上面方式一样
有了列表初始化和initializer_list 我们使用起来就很方便了 如下对于map<string,string>类型 在创建对象的时候就可以用两个pair类型的数据进行构造初始化
右值引用和移动语义
左值和右值
左值和右值区别其实就是在生命周期上
• 左值是⼀个表示数据的表达式(如变量名或解引用的指针) ⼀般是有持久状态 存储在内存中,可以获取它的地址,左值可以出现赋值符号的左边,也可以出现在赋值符号右边。定义时const 修饰符后的左值,不能给他赋值,但是可以取它的地址。
• 右值也是⼀个表示数据的表达式 要么是常量、要么是表达式求值过程中创建的临时对象 等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。
例如下面的一些例子
左值引用和右值引用
之前学习中使用到的引用 Type& a=x 这种就是左值引用 Type&& b=y 这种用了两个&的方式就是右值引用
右值引用和之前使用的左值引用一样 都是给变量取别名
左值引用可以直接给左值取别名 右值引用可以直接给右值取别名
左值引用不能直接引用右值,但是const左值引用可以引用右值
右值引用不能直接引用左值,但是右值引用可以引用move(左值)
下面是一些例子
----左值引用不能直接给右值取别名
---但是const左值引用可以引用右值
---右值引用也不能直接引用左值
---但是左值通过move就可以了
move 涉及到引用折叠的概念 这里可以先简单理解为强转
这里注意 move之后的结果是右值 但是move的参数还是左值 如上面的move(b)的结果是右值 但是这个b没有改变 还是左值
很神奇的是 move(b)是右值 但是对其右值引用的rr1却是一个左值 如下图可以证明这一点
用左值引用可以对它直接引用 右值引用不行 move之后可以
刚开始可能觉得很奇怪为什么要这样设计 在之后的移动语义使用上才能理解为什么要这样设计
语法层面看
左值引用和右值引用都是取别名,不开空间。从汇编底层的角度看底层都是用指针实现的,没什么区别。
引用延长生命周期
右值引用可⽤于为临时对象延长生命周期,const的左值引⽤也能延长临时对象生梦周期,但这些对象无法被修改
左值和右值的参数匹配
C++98中,我们实现⼀个const左值引用作为参数的函数,那么实参传递左值和右值都可以匹配。
C++11以后,分别重载左值引用、const左值引用、右值引用作为形参的f函数
那么 就会优先匹配最合适的 如下图的例子
下面也可以证明右值引用是左值
1是临时变量 是一个左值 x是1的右值引用 x作为实参调用的函数是左值版本的 move(x)是右值 此时调用的就是右值的函数 所以x的属性是左值即右值引用是左值
右值引用和移动语义的使用场景
左值引用主要使用场景回顾
左值引⽤主要使⽤场景是在函数中左值引用传参和左值引用传返回值时减少拷贝,同时还可以修改实参和修改返回对象的价值。 但是有一种问题无法解决的
如下图 在函数里面创建了一个string类型的str调用了一次构造函数 在返回时候会先通过拷贝构造创建一个临时对象 然后临时对象再通过拷贝构造给返回值(因为在这个函数结束之后这个函数的函数栈帧会销毁)
这样就需要进行两次拷贝构造 如果这个返回值是普通的内置类型或者需要浅拷贝的自定义类型拷贝的代价不大
但是如果是需要深拷贝的自定义类型 拷贝的代价就很大了
C++98中的解决方式只有使用输出型参数解决 如下图
renum其实应该是最后要得到的结果 这里用传引用的方式作为函数第三个形参 通过改变形参来改变实参的方式
但是输出型参数这样的方式牺牲了代码的可读性
那么可以通过左值引用或者右值引用的方式来解决吗?
都不可以 如下
----右值引用做返回值的方式会直接报错 因为str是一个左值
------使用左值引用做返回值的方式虽然没有报错 但是最后并没有按我们预期打印出结果 因为其实出错了 因为str是在这个函数里面创建的 在函数栈帧结束之后就会把它释放 无法用左值引用方式来延长生命周期
所以只凭借左值引用右值引用并不能解决返回函数内部对象时候拷贝构造消耗的问题
接下来学习的移动语义就可以解决这样的问题
移动构造和移动赋值
如下图
s2的构造用到的参数ss是左值 调用了一次拷贝构造
ss和s3的构造用到的参数是右值 都是调用了一次构造一次拷贝构造参数(这里只显示了一次构造是因为编译器优化的原因 实际上是先调用了构造 然后调用了拷贝构造)
如果传的参数和上面的ss那样是左值 那么就正常调用拷贝构造
但是如果传的这个参数是右值的话 也就是马上要销毁的对象例如表达式或者匿名对象 可以用下面移动构造的方式------直接和传引用的形参交换内容(反正传的参数是右值 马上就要销毁了 不如直接用这种方式把马上要销毁对象的内容给直接拿过来)
这里就解释了为什么右值引用是左值的问题------如果这里右值引用是右值的话在这里就无法这样使用了 因为要支持这样的使用所以设计右值引用是左值
这样就避免了拷贝构造的消耗 只有交换的消耗 对于这里的string就只需要交换一个指针和两个size_t类型的数据 这样像map set那些也只需要交换根节点和size两个变量 消耗很小
移动语义对于需要深拷贝的自定义类型map set string vector这些有意义
对于浅拷贝的自定义类型Date pair<int,int>及内置类型没用
但是这里我们要注意一个问题
如下图s5的构造 移动构造的参数ss是move后的左值 此时的参数move(ss)就是右值 会调用移动构造进行资源的掠夺 把ss的内容给抢走 此时的ss里面内容就空了
所以对左值使用move的时候要小心这一点
移动赋值也是类似的
所以用移动语义就可以解决上面返回值消耗大的问题
右值引用和移动语义解决传值返回问题
从str到返回值需要进行两次拷贝 在vs2019下会优化成一次拷贝构造
这里有了移动构造后就不需要拷贝构造 而是移动构造 虽然str在函数里面是左值 但是这里会被编译器识别为右值(因为这里的str是将要销毁的对象) -------(在vs2022下因为优化更多 str的构造和两次拷贝构造优化成一次构造 看不到这样的现象)
当然对于不同的编译器会进行不同程度的优化
如vs2019下 str的构造和返回需要的两次拷贝构造会优化成一次构造一次拷贝构造
在vs2022下会直接优化成一次构造
----不优化的情况可以通过linux下通过指令控制不优化来观察 第一种优化的情况在vs2019下 第二种优化的情况在vs2022下
接下来对不同场景在不同编译器下的情况分析一下
①右值对象构造,只有拷贝构造,没有移动构造的场景
不优化的情况需要两次拷贝 vs2019的优化需要一次拷贝 vs2022的不需要拷贝
②右值对象构造,有拷贝构造,也有移动构造的场景
不优化需要两次移动构造 vs19优化后需要一次移动构造 vs2022不需要移动构造
可以看到对于①②的情况如果不是vs2022那样极致优化的情况 只有拷贝构造的这种场景下一定需要用到拷贝构造 而有了移动构造之后拷贝构造可以优化为移动构造 对于需要深拷贝的自定义类型而言 提高了效率
但是也会发现对于vs2022的情况这种合三为一的极致优化直接不需要拷贝构造了 那么此时这里的移动构造不是就没有意义了吗
来看一下接下来的情况
③右值对象赋值,只有拷贝构造和拷贝赋值,没有移动构造和移动赋值的场景
在不优化的情况下 需要一次拷贝构造一次拷贝赋值 vs2019优化需要一次拷贝赋值 vs2022也需要一次拷贝赋值
④右值对象赋值,既有拷⻉构造和拷⻉赋值,也有移动构造和移动赋值的场景
在不优化的情况下需要一次移动构造一次移动赋值 vs19优化下需要一次移动赋值 vs2022也需要一次移动赋值
可以看到③④的情况 即使在vs2022的极致优化下 如果没有移动语义 仍然需要一次拷贝赋值而有了移动赋值后就可以把其优化为移动赋值
况且优化与否是由编译器决定的 我们的代码如果换了一个编译器可能效果就改变了 但是c++11这里的移动语义只要编译器支持了c++11的这种语法 不管编译器是怎样的优化 代码都是没有什么问题的
所以移动语义的意义是无需质疑的
总结
①左值引用和右值引用最终的目的都是为了减少拷贝 提高效率
②左值引用还可以用来修改参数/返回值
但是对于函数局部对象做返回值的场景下 引用的方式不能解决 只能传值返回
解决方式
1.输出型参数(可读性降低)
2.编译器的优化(不同编译器优化情况不同)
3.c++语法的发展
类型分类(了解了解就行)
• C++11以后,进⼀步对类型进⾏了划分,右值被划分纯右值(purevalue,简称prvalue)和将亡值 (expiring value,简称xvalue)。
• 纯右值是指那些字面值常量或求值结果相当于字面值或是⼀个不具名的临时对象。如 12、true 、 nullptr 或者Add(a,b)、a+b 传值返回函数调用,或者整型 a, b , a++ , a+b 等。纯右值和将亡值C++11中提出的,C++11中的纯右值概念划分等价于 C++98中的右值。
• 将亡值是指返回右值引⽤的函数的调用表达式和转换为右值引⽤的转换函数的调用表达,如 move(x) 、 static_cast(x)(强转)
• 泛左值(generalizedvalue,简称glvalue),泛左值包含将亡值和左值。
引用折叠
简单说就是当同时有两个引用出现的时候应该是什么引用 不过不是直接定义引用的引用如 int& && r = i; ,这样写会直接报错,而是通过模板或typedef 中的类型操作可以构成引⽤的引⽤。
如下 可以看到 左值引用和右值引用折叠的结果是左值引用 只有右值引用和右值引用折叠的结果才是右值引用
右值引用的右值引用折叠成右值引用,所有其他组合均折叠成左值引用
基于此 下面f1函数的形参一定是左值引用
f2函数的形参可能是左值引用可能是右值引用
对于f2这个函数模版 实例化传的是左值则最终就是左值引用 传的是右值引用 则最终就是右值引用 所以f2这种函数模版也被称为万能引用
自动推导
上面的引用折叠是通过实例化的方式 这里用自动推导的方式
下面的Function(T&&t)函数模板中,假设实参是int右值,模板参数T的推导int,实参是int左值,模 板参数T的推导int&,再结合引用折叠规则,就实现了实参是左值,实例化出左值引⽤版本形参的 Function,实参是右值,实例化出右值引⽤版本形参的Function。
一些地方对于右值需要移动语义 而左值和右值需要分两个来写 右值引用折叠产生的万能模版解决了这个问题
完美转发
下面这样的函数嵌套的场景有一个问题 不管是左值引用还是右值引用 其本身都是左值
那么在函数里面调用其他函数的时候调用的一定是左值引用版本 如下面func函数中的形参c 这里的c一定是左值则里面的Fun函数调用的就一定是左值的版本
如下用到完美转发就可以解决这样的问题
通过前面知道 传的是右值 T就是int 传的是左值T就是int&
完美转发forward本质是⼀个函数模板 通过引用折叠的方式实现,在forward<T>那里会根据识别到的来进行强转 外层传的是左值 则在完美转发后就是左值 传的是右值 完美转发后就是右值 会保留它原来的属性
在c++11中容器是支持移动构造的 如下面的list
所以用库中的list来使用push_back对于左值会进行拷贝构造 而对于右值进行的是移动构造
如果使用之前自己实现的list的时候 结果都是拷贝构造 因为自己实现的list还没有实现移动构造
(测试时候发现崩了 因为创建的string类型的s1被释放了两次 在main函数结束之后 自动先调用了自己写string的析构函数 然后list的析构函数会把里面的资源释放 因为插入是传引用方式插入的 所以list析构函数再把s1的空间给释放一次 就崩了)
那么接下来就结合学到的内容来自己实现一下list的移动构造
手动实现list的移动构造
解决方式一
再写一个insert专门处理右值插入的情况 还需要用到Node的构造函数 所以Node的构造函数也需要专门写一个处理右值的版本
此时还有一个问题 之前我们知道了右值引用是左值 传给Node一定是左值 在Node里面用data的时候这个data也一定是左值 所以调用的会是string的左值版本的拷贝构造 所以对于每一个地方都需要move一下转为右值
push_back的逻辑是复用insert的 所以push_bakc实现支持移动的方式也是 重写一个专门针对右值的版本
这种方式没有什么问题 但是有一个缺陷 --------我们需要专门写一个针对右值版本的函数 而且上面可以看到里面可能是多层的 例如push_back支持右值版本的方式 push_back insert 及Node节点的构造都需要重新一份 那这样就使得代码变得很冗余了
解决方式二
结合我们学到的引用折叠及完美转发可以解决这个问题且不会有代码冗余的问题
这种方式如果插入的是右值也可以正常调用移动构造了 而且不像第一种方法那样的代码冗余