[C++]现代C++语言核心特性解析 Chapter 6

右值引用(C++11 C++17 C++20)

1 左值和右值

在C++中

  • 左值一般是指一个指向特定内存的具有名称的值(具名对象),它有一个相对稳定的内存地址,并且有一段较长的生命周期。
  • 右值则是不指向稳定内存地址的匿名值(不具名对象),它的生命周期很短,通常是暂时性的。

基于这一特征,可以用取地址符&来判断左值和右值,能取到内存地址的值为左值,否则为右值。

除字符串字面量以外的字面量,通常都是右值。编译器会将字符串字面量存储到程序的数据段中,程序加载的时候也会为其开辟内存空间,所以我们可以使用取地址符&来获取字符串字面量的内存地址。

2 左值引用

左值引用在传参的时候经常使用,可以免去创建一个临时对象的操作。

非常量左值引用的引用对象必须是一个左值

常量左值引用的引用对象可以引用右值。这个特性很有用,在作为参数的时候,可以接受一个右值,但是这会导致参数的常量性,需要右值引用来解决。

3 右值引用

右值引用是一种引用右值且只能引用右值的方法。在语法方面右值引用可以对比左值引用,在左值引用声明中,需要在类型后添加&,而右值引用则是在类型后添加&&

右值引用的特点之一是可以延长右值的生命周期。以此达到减少复制,提升性能的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# include <iostream>

class X {
public:
X() { std::cout << "X ctor" << std::endl; }
X(const X&x) { std::cout << "X copy ctor" << std::endl; }
~X() { std::cout << "X dtor" << std::endl; }
void show() { std::cout << "show X" << std::endl; }
};

X make_x()
{
X x1;
return x1;
}

int main()
{
X &&x2 = make_x();
x2.show();
}

如果执行 X x2 = make_x();,在没有任何优化的情况下,会执行三次构造函数,首先构造x1,然后return的时候执行拷贝构造函数构造一个临时对象,最后赋值的时候调用拷贝构造函数。

如果执行 X &&x2 = make_x();,则只会执行两次构造函数,前两次和上面一样,但是最后由于x2右值引用了临时对象,因此这个临时对象的生命期得以延长。

4 移动语义

c++11标准提供了移动语义,可以将资源在对象中进行转移,即进行浅拷贝的工作。

使用的时候有两点需要注意:

  1. 同复制构造函数一样,编译器在一些条件下会生成一份移动构造函数,这些条件包括:没有任何的复制函数,包括复制构造函数和复制赋值函数;没有任何的移动函数,包括移动构造函数和移动赋值函数;也没有析构函数。虽然这些条件严苛得让人有些不太愉快,但是我们也不必对生成的移动构造函数有太多期待,因为编译器生成的移动构造函数和复制构造函数并没有什么区别。
  2. 虽然使用移动语义在性能上有很大收益,但是却也有一些风险,这些风险来自异常。试想一下,在一个移动构造函数中,如果当一个对象的资源移动到另一个对象时发生了异常,也就是说对象的一部分发生了转移而另一部分没有,这就会造成源对象和目标对象都不完整的情况发生,这种情况的后果是无法预测的。所以在编写移动语义的函数时建议确保函数不会抛出异常,与此同时,如果无法保证移动构造函数不会抛出异常,可以使用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
2
3
4
5
6
7
8
9
10
11
void move_pool(BigMemoryPool &&pool)
{
std::cout << "call move_pool" << std::endl;
BigMemoryPool my_pool(pool); // 调用拷贝构造函数
BigMemoryPool my_pool1(static_cast<BigMemoryPool&&>(pool)); // 调用移动构造函数
}

int main()
{
move_pool(make_pool());
}

在这个代码中,虽然 make_pool 返回的是一个临时对象,且 move_pool 的形参是一个右值引用,但是在构造 my_pool 的时候还是会执行拷贝构造函数。这是因为无论一个函数的实参是左值还是右值,即使形参是一个右值引用,这个形参本身也是一个左值。

c++11的标准库中提供了一个函数模板 std::move 来将左值转换为右值,本质也是用 static_cast 来做的转换,推荐使用 std::move

7 万能引用和引用折叠

含有 T&&auto&& 的是万能引用。在这个推导过程中,初始化的源对象如果是一个左值,则目标对象会推导出左值引用;反之如果源对象是一个右值,则会推导出右值引用,不过无论如何都会是一个引用类型。

万能引用能如此灵活地引用对象,实际上是因为在C++11中添加了一套引用叠加推导的规则——引用折叠。

8 完美转发

万能引用最典型的用途被称为完美转发。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <string>

template<class T>
void show_type(T t)
{
std::cout << typeid(t).name() << std::endl;
}

template<class T>
void normal_forwarding(T t)
{
show_type(t);
}

int main()
{
std::string s = "hello world";
normal_forwarding(s);
}

在上面这个例子中,当调用 normal_forwarding 的时候,会拷贝一次字符串,然后调用 show_type 的时候,又将拷贝一次字符串。我们可以将 normal_forwarding 的形参声明为引用来减少一次拷贝,但是这会导致函数无法接受右值。这个问题也可以通过常量左值引用来解决,但是这就会导致无法修改字符串。

有了万能引用,就可以解决这个问题。需要注意的是,因为形参t一定是一个左值,为了让转发能够将左右值属性带到目标函数中去(show_type),这里需要进行类型转换,同样用到了引用折叠的概念。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <string>

template<class T>
void show_type(T t)
{
std::cout << typeid(t).name() << std::endl;
}

template<class T>
void perfect_forwarding(T &&t)
{
show_type(static_cast<T&&>(t));
}

std::string get_string()
{
return "hi world";
}

int main()
{
std::string s = "hello world";
perfect_forwarding(s);
perfect_forwarding(get_string());
}

在c++11标准库中提供了 std::forward 函数模板,其内部也是使用 static_cast 进行的类型转换,但是采用 std::forward 使语义更加清楚。

注意 std::movestd::forward 的区别:

  • 其中 std::move 一定会将实参转换为一个右值引用,并且使用 std::move 不需要指定模板实参,模板实参是由函数调用推导出来的。
  • std::forward 会根据左值和右值的实际情况进行转发,在使用的时候需要指定模板实参

[C++]现代C++语言核心特性解析 Chapter 6
https://erlsrnby04.github.io/2024/10/20/C-现代C-语言核心特性解析-Chapter-6/
作者
ErlsrnBy04
发布于
2024年10月20日
许可协议