0%

左值引用,右值引用,移动语义,完美转发

本文主要记录左值引用与 C++11 中引入的右值引用,以及右值引用相关的应用:移动语义与完美转发。

左值与右值

左值与右值的区分标准在于能否获取其在内存中的地址。

左值(lvalue):可以获取地址的对象。

右值(rvalue):无法获取地址的对象,如常量、函数返回值、Lambda 表达式等。

1
int i = 10; // i 是左值,10 是右值

右值虽然无法获取地址,但是不代表不可以改变其值;当定义了右值对象的右值引用时,就可以获取对象的地址,也就可以改变对象的值了。

左值引用与右值引用

传统的 C++ 引用被称为左值引用,用法如下:

1
2
int i = 10;
int &lvalue_ref = i;

C++ 11 中引入了右值引用的概念,当右值引用绑定到右值时,右值被存储到特定位置,右值引用指向该特定位置,用法如下:

1
int &&rvalue_ref = 10;

右值虽然无法获取地址,但是右值引用是可以获取地址的,该地址表示临时对象的存储位置。

右值引用的应用场景:移动语义完美转发

const 修饰的左值引用

定义一个左值引用,将其值设置为一个常量值,则会报错:

1
int &i = 10; // ERROR

变量 i 是一个左值引用,而常量 10 是一个右值,因此无法将左值引用绑定到一个右值上。

但如果 i 是一个用 const 修饰的左值引用,那么是可以绑定到右值上的:

1
const int &i = 10;

常量左值引用 (const &) 可以接受常量左值 (const lvalue),非常量左值 (lvalue),右值 (rvalue)。

在早期的 C++ 中,引用并无左右之分,引入了右值引用后,传统的引用才被称为左值引用。

因此左值引用其实可以绑定任何对象,这也是为什么 const 修饰的左值引用可以绑定常量值。

1
2
3
4
int i = 10;
int &lvalue_ref = i;
int &&rvalue_ref = 10;
lvalue_ref = rvalue_ref;

移动语义

右值引用是用来支持移动语义的。移动语义可以将资源从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 的性能。

移动语义是和拷贝语义相对的,就像文件的剪切与拷贝,当我们将文件从一个目录拷贝到另一个目录时,速度会比剪切慢很多。

实例分析

定义 point 类:

1
2
3
4
class point {
private:
int *_value;
};

拷贝构造函数:

1
2
3
4
point(const point &pt) {
_value = new int;
memcpy(_value, pt._value, sizeof(int)); // 深拷贝指针
}

需要深拷贝指针,浅拷贝可能会导致多次释放内存,从而引起错误。

移动构造函数:

1
2
3
4
point(point &&pt) {
_value = pt._value; // 移动指针
pt._value = nullptr; // 临时对象的指针置空
}

临时对象的指针需要置空,防止临时对象释放资源时,影响已经转移给新对象的资源。

拷贝赋值运算符重载:

1
2
3
4
5
6
7
8
9
point &operator=(const point &pt) {
if (this == &pt) {
return *this;
}
delete _value;
_value = new int;
memcpy(_value, pt._value, sizeof(int));
return *this;
}

移动赋值运算符重载:

1
2
3
4
5
6
7
8
point &operator=(point &&pt) {
if (this == &pt) {
return *this;
}
_value = pt._value;
pt._value = nullptr;
return *this;
}

完美转发

首先看两条规则,这两条规则决定了引用类型参数的函数传参方式。

引用折叠规则

C++ 中存在“引用的引用”(reference to reference),分为以下 4 种情况:

  1. lvalue reference to lvalue reference(左值引用绑定到左值引用)
  2. lvalue reference to rvalue reference(左值引用绑定到右值引用)
  3. rvalue reference to lvalue reference(右值引用绑定到左值引用)
  4. rvalue reference to rvalue reference(右值引用绑定到右值引用)

但是由于 C++ 中不允许“引用的引用”的存在,所以编译器会根据引用折叠规则(Reference Collapsing),将这 4 种情况转换为单引用(Single Reference)。其中,情况 1、2、3 会被转化为左值引用,情况 4 会被转化为右值引用。

模板函数对右值引用参数(T &&)的类型推导规则

模板函数定义如下:

1
2
template <typename T>
void func(T &&t) { }

向一个模板函数传递一个左值实参,且该模板函数的对应形参为右值引用时,编译器会把该实参推导为左值引用。具体情况见下表:

实参类型 T 的类型 形参类型
具名对象(左值) T & T &
不具名对象(右值) T T &&
具名左值引用(左值) T & T &
不具名左值引用(左值 T & T &
具名右值引用(左值) T & T &
不具名右值引用(右值) T T &&

实例分析

定义 point 类:

1
2
3
4
5
6
7
class point {
public:
point() { cout << "constructor default" << endl; }
point(const point &pt) { cout << "constructor copy" << endl; }
point(point &&pt) { cout << "constructor move" << endl; }
~point() { cout << "destructor" << endl; }
};

定义下面的两个函数:

1
2
3
4
5
6
7
8
9
10
11
template <typename T>
void func_sub(T t) {
cout << "sub" << endl;
return;
}

template <typename T>
void func_main(T &&t) {
cout << "main" << endl;
func_sub(t); // 在 func_main 中调用 func_sub
}

调用函数 func_main 传入一个 point 类的临时对象(右值):

1
func_main(point());

得到输出如下:

1
2
3
4
5
6
constructor default // 默认构造临时对象
main // func_main 函数
constructor copy // 拷贝构造 func_sub 的形参
sub // func_sub 函数
destructor // func_sub 的形参析构
destructor // 临时对象析构

由于 func_main 函数中的 t 是具名右值引用,因此 t 是左值,传入 func_sub 函数的实参也就是左值了,而 func_sub 函数的形参是值传递形式,所以会调用 point 类的拷贝构造函数。

如果想要调用 point 类的移动构造函数,则需要传入右值。完美转发函数 std::forward 就提供了这样的功能(完美地保留变量的左右值属性并进行转发)。我们对 func_main 进行一定的修改:

1
2
3
4
template <typename T> void func_main(T &&t) {
cout << "main" << endl;
func_sub(forward<T>(t));
}

这样一来,右值引用所绑定的变量的右值属性得到了保留,并传入 func_sub 函数,从而调用 point 类的移动构造函数对 func_sub 的形参进行构造。

再次运行程序,可以得到输出如下:

1
2
3
4
5
6
constructor default // 默认构造临时对象
main // func_main 函数
constructor move // 移动构造 func_sub 的形参
sub // func_sub 函数
destructor // func_sub 的形参析构
destructor // 临时对象析构