先看以下代码,但凡使用过 C++ 的人应该都很清楚,第三行代码是无法通过编译的,原因是非常量引用只能绑定在左值上。这里的左值表示有具体物理内存地址的值,即变量,因此使用符号 “&” 表示只能绑定在具体变量上的引用被称作左值引用。
int num_1 = 1;
int &num_2 = num_1;
int &num_3 = 1;
根据以下代码,在 C++11/C++0x 标准以前,如果需要将该左值引用绑定到一个常量上,通常的做法是使用常量引用以延长临时变量的生存期,这个操作在内存中同样也开辟了一块内存空间用于储存 1 和 get 函数的返回值。但遗憾的是,这些值都属于常量,无法被修改。
const int &num = 1;
const int &num = get(); // 设 get 函数的定义:int get() { return 1; }
在 C++11/C++0x 标准中提出了右值引用,与左值引用不同,右值引用绑定的对象是一个字面值、临时变量、将亡值(即右值)与常量引用类似,右值引用也能延长该临时值的生存期,但不同的是,所绑定的值是可以通过该右值引用修改的。可参考以下代码:
// 右值引用绑定在字面值 1 上,以延长该字面值 1 的生存期
int &&num = 1;
// 右值引用绑定在上临时值上
int &&num = get(); // 设 get 函数的定义:int get() { return 1; }
// 可随时对已被延长生存期的右值引用进行访问,右值引用变量在用于表达式时是左值
num = 2;
tips: 左值可以出现在赋值号 “=” 左边或者右边,而右值只可以出现在赋值号 “=” 右边。
拿一个不太恰当但容易理解的比喻:可以看做左值是个未爆炸的炸弹,右值是个即将爆炸的炸弹。
我们首先定义一个类,这个类封装了一个数组,并且提供有参、复制两个构造函数
class MyArray
{
int *_arr;
int _size;
public:
// 有参构造
explicit MyArray(int size) : _size(size)
{
cout << "parameter constructor" << endl;
_arr = new int[size];
}
// 复制构造
MyArray(const MyArray& my_array) : _size(my_array._size)
{
cout << "copy constructor" << endl;
_arr = new int[_size];
// 复制每一个元素
for (int i = 0; i < _size; i++)
_arr[i] = my_array._arr[i];
}
// 析构
~MyArray()
{
cout << "destructor" << endl;
if (_arr != nullptr)
{
delete _arr;
_arr = nullptr;
}
}
inline int &operator[](size_t __n) { return _arr[__n]; }
};
在 main 函数中写入
int main(int argc, char *argv[]) // line 1
{ // line 2
vector<MyArray> arrs; // line 3
arrs.reserve(5); // line 4
MyArray arr(1); // line 5
arrs.push_back(arr); // line 6
arrs.push_back(MyArray(1)); // line 7
return 0; // line 8
} // line 9
运行结果如下:
MyArray(1)
运行的结果,其创建了一个临时变量,调用有参构造parameter constructor
copy constructor
parameter constructor
copy constructor
destructor
destructor
destructor
destructor
可以看到,对于临时变量的处理,传统的 C++ 显得十分笨拙,即创建一个新的空间,将该临时变量逐一复制到新空间,再销毁该临时变量,对于临时变量而言,如果能够将其数据所有权直接交给新空间,其物理内存地址不发生任何变动,那么将节省大量拷贝所浪费的时间。
其中,这个移交所有权的过程,在 C++ 中可以表示成将指针直接指向这个临时变量所在的地址,再让该临时变量“指向”空。这里的“指向”并不是C语言中指针指向某个地址的意思,而是该临时变量的变量名直接取址所指向的就是这块地址。这个操作可以看成该临时变量的内容直接移动到新空间中。
利用右值和右值引用的特点,可以加入一个新的构造函数,被称作移动构造函数。在上文 MyArray 类的代码中加入以下内容:
// move constructor
MyArray(MyArray &&my_array) : _size(my_array._size)
{
cout << "move constructor" << endl;
_arr = my_array._arr;
my_array._arr = nullptr;
}
运行结果如下,在运行中传入push_back的参数为一个临时变量,因此编译器优先匹配移动构造函数,因此第四行为移动构造函数的运行结果
parameter constructor
copy constructor
parameter constructor
move constructor
destructor
destructor
destructor
destructor
在 C++11 中提供了一个函数:std::move()
,其作用就是将为了移交变量对于地址访问的所有权,也就是将左值变成右值,其做的转换等价于static_cast
,因此不会修改变量的值。现给出以下代码,同样以上文的 MyArray 类为例
int main(int argc, char *argv[])
{
MyArray arr1(1);
MyArray arr2(arr1);
MyArray arr3(move(arr1));
MyArray &&tmp = move(arr2);
MyArray arr4(tmp);
return 0;
}
运行结果如下:
首先,程序第一行调用有参构造函数。(结果第1行)
第二行利用现有的 arr1 构造 arr2,同时 arr1 不受影响,调用复制构造函数。(结果第2行)
然后程序调用了std::move()
,因此其得到了一个右值(临时值)尝试点炸弹
并且将其传给 arr3 调用移动构造,构造完成后 arr1 的数组内容将被释放。(结果第3行) 炸弹点着
接着调用std::move()
,得到了 arr2 的临时值,该临时值的生存期得以延长。尝试点炸弹
最后,访问 tmp 时,此处 tmp 已不再是右值,而是 arr2 的左值引用,因此调用复制构造函数。(结果第4行) 炸弹没点着
parameter constructor
copy constructor
move constructor
copy constructor
destructor
destructor
destructor
destructor
C++11开始便提供了引用折叠机制,具体示例代码如下
using lref = int &;
using rref = int &&;
int n = 10;
lref &r1 = n; // r1 的类型是 int&
lref &&r2 = n; // r2 的类型是 int&
rref &r3 = n; // r3 的类型是 int&
rref &&r4 = 1; // r4 的类型是 int&&
转发引用(也称作万能引用)是一种特殊的引用,具有下列两种情形:
函数模板的函数形参,其被声明为同一函数模板的类型模板形参的无 cv 限定( const
以及 volatile
)的右值引用(T &&
)
template <typename T>
int foo(T &&x) { return x; }
template <typename T>
int f(T &&x) { return foo(forward<T>(x)); } // x 是转发引用,从而能被转发
int main()
{
int i = 10;
f(i); // 实参是左值,调用 f(int &), std::forward(x) 是左值
f(5); // 实参是右值,调用 f(int &&), std::forward(x) 是右值
}
template <typename T>
int g(const T &&x) { return x; } // x 不是转发引用:T 有 cv 限定
template <typename T>
struct A
{
template <typename U>
A(T &&x, U &&y, int *p); // x 不是转发引用:T 不是该构造函数的类型模板形参,但 y 是转发引用
};
auto &&
,但当其从花括号包围的初始化器列表推导时除外
auto &&vec = x_f(); // x_f() 可以是左值或右值,vec 是转发引用
auto i = std::begin(vec); // 也可以
(*i)++; // 也可以
x_g(std::forward<decltype(vec)>(vec)); // 转发,保持值类别
for (auto &&x : x_h())
{
// x 是转发引用;这是使用范围 for 循环最安全的方式
}
auto &&z = {1, 2, 3}; // 不是转发引用(初始化器列表的特殊情形)
总结:万能引用能保持函数实参的值类别,使得 std::forward 能用来完美转发实参。
由于笔者水平有限,文中难免会有错误和疏漏之处,烦请各位读者评论指出!