[C++]现代C++语言核心特性解析 Chapter 6
右值引用(C++11 C++17 C++20)
1 左值和右值
在C++中
- 左值一般是指一个指向特定内存的具有名称的值(具名对象),它有一个相对稳定的内存地址,并且有一段较长的生命周期。
- 右值则是不指向稳定内存地址的匿名值(不具名对象),它的生命周期很短,通常是暂时性的。
基于这一特征,可以用取地址符&来判断左值和右值,能取到内存地址的值为左值,否则为右值。
除字符串字面量以外的字面量,通常都是右值。编译器会将字符串字面量存储到程序的数据段中,程序加载的时候也会为其开辟内存空间,所以我们可以使用取地址符&来获取字符串字面量的内存地址。
2 左值引用
左值引用在传参的时候经常使用,可以免去创建一个临时对象的操作。
非常量左值引用的引用对象必须是一个左值
常量左值引用的引用对象可以引用右值。这个特性很有用,在作为参数的时候,可以接受一个右值,但是这会导致参数的常量性,需要右值引用来解决。
3 右值引用
右值引用是一种引用右值且只能引用右值的方法。在语法方面右值引用可以对比左值引用,在左值引用声明中,需要在类型后添加&,而右值引用则是在类型后添加&&
右值引用的特点之一是可以延长右值的生命周期。以此达到减少复制,提升性能的效果。
1 |
|
如果执行 X x2 = make_x();
,在没有任何优化的情况下,会执行三次构造函数,首先构造x1,然后return的时候执行拷贝构造函数构造一个临时对象,最后赋值的时候调用拷贝构造函数。
如果执行 X &&x2 = make_x();
,则只会执行两次构造函数,前两次和上面一样,但是最后由于x2右值引用了临时对象,因此这个临时对象的生命期得以延长。
4 移动语义
c++11标准提供了移动语义,可以将资源在对象中进行转移,即进行浅拷贝的工作。
使用的时候有两点需要注意:
- 同复制构造函数一样,编译器在一些条件下会生成一份移动构造函数,这些条件包括:没有任何的复制函数,包括复制构造函数和复制赋值函数;没有任何的移动函数,包括移动构造函数和移动赋值函数;也没有析构函数。虽然这些条件严苛得让人有些不太愉快,但是我们也不必对生成的移动构造函数有太多期待,因为编译器生成的移动构造函数和复制构造函数并没有什么区别。
- 虽然使用移动语义在性能上有很大收益,但是却也有一些风险,这些风险来自异常。试想一下,在一个移动构造函数中,如果当一个对象的资源移动到另一个对象时发生了异常,也就是说对象的一部分发生了转移而另一部分没有,这就会造成源对象和目标对象都不完整的情况发生,这种情况的后果是无法预测的。所以在编写移动语义的函数时建议确保函数不会抛出异常,与此同时,如果无法保证移动构造函数不会抛出异常,可以使用noexcept说明符限制该函数。这样当函数抛出异常的时候,程序不会再继续执行而是调用std::terminate中止执行以免造成其他不良影响。
5 值类别
值类别是C++11标准中新引入的概念,具体来说它是表达式的一种属性,该属性将表达式分为3个类别,它们分别是左值(lvalue)、纯右值(prvalue)和将亡值(xvalue)。
实际上,这里的左值(lvalue)就是上文中描述的C++98的左值,而这里的纯右值(prvalue)则对应上文中描述的C++98的右值。
将亡值(Xvalue,Expiring value)是表示即将“被销毁”的值,通常是资源可以被移动的对象。将亡值允许资源被“移动”,即允许将其内部的资源转移到另一个对象中,而不是拷贝。
特点:
- 可以被移动构造或移动赋值。
- 典型的将亡值包括:返回右值引用的表达式、
std::move
转换后的对象。
6 将左值转换为右值
在c++11中,我们可以使用 static_cast<Foo&&>(foo)
将一个左值转换为一个将亡值,然后将右值引用绑定到这个将亡值上。此外,我们可以通过这种方式让左值使用移动语义。需要注意的是,转换前后,对象具有相同的内存地址和生命周期。
1 |
|
在这个代码中,虽然 make_pool
返回的是一个临时对象,且 move_pool
的形参是一个右值引用,但是在构造 my_pool
的时候还是会执行拷贝构造函数。这是因为无论一个函数的实参是左值还是右值,即使形参是一个右值引用,这个形参本身也是一个左值。
c++11的标准库中提供了一个函数模板 std::move
来将左值转换为右值,本质也是用 static_cast
来做的转换,推荐使用 std::move
。
7 万能引用和引用折叠
含有 T&&
和 auto&&
的是万能引用。在这个推导过程中,初始化的源对象如果是一个左值,则目标对象会推导出左值引用;反之如果源对象是一个右值,则会推导出右值引用,不过无论如何都会是一个引用类型。
万能引用能如此灵活地引用对象,实际上是因为在C++11中添加了一套引用叠加推导的规则——引用折叠。
8 完美转发
万能引用最典型的用途被称为完美转发。
1 |
|
在上面这个例子中,当调用 normal_forwarding
的时候,会拷贝一次字符串,然后调用 show_type
的时候,又将拷贝一次字符串。我们可以将 normal_forwarding
的形参声明为引用来减少一次拷贝,但是这会导致函数无法接受右值。这个问题也可以通过常量左值引用来解决,但是这就会导致无法修改字符串。
有了万能引用,就可以解决这个问题。需要注意的是,因为形参t一定是一个左值,为了让转发能够将左右值属性带到目标函数中去(show_type
),这里需要进行类型转换,同样用到了引用折叠的概念。
1 |
|
在c++11标准库中提供了 std::forward
函数模板,其内部也是使用 static_cast
进行的类型转换,但是采用 std::forward
使语义更加清楚。
注意 std::move
和 std::forward
的区别:
- 其中
std::move
一定会将实参转换为一个右值引用,并且使用std::move
不需要指定模板实参,模板实参是由函数调用推导出来的。 - 而
std::forward
会根据左值和右值的实际情况进行转发,在使用的时候需要指定模板实参。