前言
本文介绍C++11的各种引用的概念,理解清楚各种引用的概念,非常有助于理解基于c11引用的各种操作。
左右值概念
C++ 里有左值和右值,但C++按标准里的定义实际更复杂,规定了下面这些值类别(value categories):
-
名词解释:
一个 lvalue 是通常可以放在等号左边的表达式,左值
一个 rvalue 是通常只能放在等号右边的表达式,右值
一个 glvalue 是 generalized lvalue,广义左值
一个 xvalue 是 expiring value,将亡值
一个 prvalue 是 pure rvalue,纯右值 -
左值 lvalue 是有标识符、可以取地址的表达式,最常见的情况有:
变量、函数或数据成员的名字
返回左值引用的表达式,如 ++x、x = 1、cout << ’ ’
字符串字面量如 “hello world”
在函数调用时,左值可以绑定到左值引用的参数,如 T&。一个常量只能绑定到常左值引用,如 const T&。 -
纯右值 prvalue 是没有标识符、不可以取地址的表达式,一般也称之为临时对象,即临时对象就是一种右值,最常见的情况有:
返回非引用类型的表达式,如 x++、x + 1、make_shared(42)
除字符串字面量之外的字面量,如 42、true -
变量是左值
左右值区别
-
左值持久,右值短暂
左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。
由于右值引用只能绑定到临时对象,所以使用右值引用的代码可以自由地接管所引用的对象的资源,即可以进行移动。 -
移动右值,拷贝左值
如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数,赋值操作的情况类似。
StrVec v1, v2;
v1 = v2; // 拷贝赋值,因为v2是左值
v1 = std::move(v2); // 强制变成右值了,使用移动赋值
StrVec getVec();
v2 = getVec(); // 移动赋值,因为getVec()是右值
- 把 std::move(ptr1) 看作是一个有名字的右值。为了跟无名的纯右值 prvalue 相区别,C++ 里目前就把这种表达式叫做 xvalue。
跟左值 lvalue 不同,xvalue 仍然是不能取地址的——这点上,xvalue 和 prvalue 相同。所以,xvalue 和 prvalue 都被归为右值 rvalue。
shared_ptr<shape> ptr1{new circle()}; // new circle() 就是一个纯右值
shared_ptr<shape> ptr2 = std::move(ptr1); // 可以把 std::move(ptr1) 看作是一个有名字的右值。为了跟无名的纯右值 prvalue 相区别,C++ 里目前就把这种表达式叫做 xvalue。
左值引用lvalue reference
当我们使用术语引用reference时,指的其实是左值引用(lvalue reference)。
-
引用即别名,引用并非对象,它是为一个已经存在的对象所起的另一个名字。定义了一个引用之后,对其进行的所有操作都是在与之绑定的对象上进行的。
-
引用必须初始化,一旦初始化完成,引用将和它的初始值对象一直绑定在一起:
引用在初始化后,不可以改变;
引用类型必须和绑定的对象的类型一致,但常量引用会丢失精度;
引用类型的初始值必须是一个变量;
常量引用可以引用常量; -
引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起;
int c = 2;
ra = c; // 赋值操作,把c赋值给b,即赋值给a
// &ra = c; // 错误,引用在初始化后,不可以改变
// Non-const lvalue reference to type 'int' cannot bind to a temporary of type 'int'
const int e = 10;
// int &re = e; // 错误,引用类型的初始值必须是一个对象
const int &re = e;
// int &re1 = e * 42; // 错误,引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起,a * 42是一个右值
const int &re1 = e * 42; // 正确,可以将一个const引用绑定到一个右值上
double d = 10.0F;
// Non-const lvalue reference to type 'int' cannot bind to a value of unrelated type 'double'
// int &rd = d; // 错误,引用类型必须和绑定的对象的类型一致,添加const后可以
double &rd = d;
const int &rd1 = d; // 正确,Clang-Tidy: Narrowing conversion from 'double' to 'int'
右值引用rvalue reference
为了支持移动操作,新标准引入了一种新的引用类型——右值引用。
-
所谓右值引用就是必须绑定到右值的引用,我们通过&&而不是&来获得右值引用。
-
右值引用有一个重要的性质:只能绑定到一个将要销毁的对象。
因此,我们可以自由地将一个右值引用的资源“移动”到另一个对象中。 -
一个右值引用也不过是某个对象的另一个名字而已。
-
对于左值引用,我们不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。
右值引用有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上。 -
返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的例子。我们可以将一个左值引用绑定到这类表达式的结果上。
返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。我们不能将一个左值引用绑定到这类表达式上,但我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。
std::move
虽然不能将一个右值引用直接绑定到一个左值上,但我们通过std::move函数可以显式地将一个左值转换为对应的右值引用类型。
-
move调用告诉编译器:我们有一个左值,但我们希望像一个右值一样处理它。
-
调用move就意味着承诺:
除了对移后源对象进行赋值或销毁它外,我们将不再使用它。
在调用move之后,我们不能对移后源对象的值做任何假设。
我们可以销毁一个移后源对象,也可以赋予它新值,但不能使用一个移后源对象的值。 -
不要随意使用移动操作
通过在类代码中小心地使用move,可以大幅度提升性能。
但由于一个移后源对象具有不确定的状态,对其调用std::move是危险的。当我们调用move时,必须绝对确认移后源对象没有其他用户。
而如果随意在普通用户代码(与类实现代码相对)中使用移动操作,很可能导致莫名其妙的、难以查找的错误,而难以提升应用程序性能。
示例
int a = 1;
int &ra = a; // 引用必须要初始化
// int &ra2 = 1; // 错误,引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起
// int &&rra = a; // 错误,不能将一个右值引用绑定到左值上
// int &ra3 = a * 42; // 错误,引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起,a * 42是一个右值
const int &ra3 = a * 42; // 正确,可以将一个const引用绑定到一个右值上
int &&rra2 = a * 42; // 正确,将rra2绑定到一个右值上
int &&rra2 = a * 42; // 正确,将rra2绑定到一个右值上
// int &&rrra = rra2; // 错误,表达式rra2是左值,变量是左值
int &&rrra = std::move(rra2); // 正确,使用std::move将左值显式的变为右值