C++ 左/右值及其引用 论述
说明: 本文探讨的是 C++11 以来的值类别
关于左值和右值,在不对其进行详细的划分时,简单的分类方法包括
- 左值持久,右值短暂
- 能取得地址得通常是左值,反之通常是右值(这一方法启示我们一个表达式的类型与其是左值还是右值无关,即相同类型的表达式既可以是左值也可以是右值)
右值引用是必须绑定到右值的引用,通过&&
获取右值引用
左值引用如果想绑定要右值上,只能通过const
,例如
int i = 1;
int &r = i; // 正确,i是一个左值,可以采用左值引用
int &r = i * 2; // 错误,i*2是一个右值,无法直接采用左值引用
const int &r = i * 2; // 正确
int &&r = i * 2; // 正确,i*2是一个右值,可以采用右值引用
C++11以前,右值被认为是无用的资源,右值引用引入的目的是为了延长用来初始化对象的生命周期(左值的生命周期和作用域相关,无需延长)
int x = 20; // 左值
int&& rx = x * 2; // x*2的结果是一个右值,rx延长其生命周期
int y = rx + 2; // 可以重用右值:42
rx = 100; // 一旦初始化一个右值引用变量,该变量就成为了一个左值,可以被赋值
以上代码一方面展示了右值引用延长生命周期的效果,同时引出了右值引用一个重要的特点,即初始化之后的右值引用将变成一个左值,如果是non-const还可以被赋值
关于"左值持久,右值短暂"这种说法,存在一个第一次看让人感到惊讶的例子,即
不能将一个右值引用绑定到一个右值引用类型的变量上
int &&r1 = 42; // 正确,字面值常量是右值
int &&r2 = r1; // 错误,表达式r1(变量可以看作只有一个运算对象而无运算符的表达式)是左值
r1
虽然是右值引用,但其本质是一个变量,是一个持久值,因此其属于左值,不能直接被右值引用绑定
不能直接将右值引用绑定到左值上,但左值引用可以通过const实现绑定右值,因此C++11引入了新标准库函数move
用于显示地将左值转换为对应右值引用类型,实现用右值类型绑定左值
#include <utility>
int &&r1 = 42; // 42为右值,r1是右值引用类型,本身属于左值
int &&r2 = std::move(r1); // 正确,move将左值转换为右值引用
move
函数有几点注意事项
- 函数所在头文件为
<utility>
- 由于右值引用能接受任意类型参数,为了避免与名为move且单参数的自定义move函数冲突,move并不提供using命名空间声明,必须使用std::move而非move
- 引用折叠的存在,使得右值引用可以接受左值和右值,因此std::move可以接受任意参数
- 对于上述代码,调用move意味着不能对移后源对象r1的数值做任何假设,不能再读取它的数值,但是可以进行赋值和销毁操作
虽然说move
函数叫做移动,但其只是起到转换作用(从实际效果来看,称其为rvalue_cast
似乎更合适),其并没有移动什么东西。对于目前"移动"的名称,更可能是因为其告知了编译器某个对象更加适合移动,所以才称其为move
同时有一个易错点是:使用了std::move
并不一定会去利用移动构造函数。在下面的例子中,虽然初始化成员属性时采用了右值,但是由于形参string采用了const,因此实际调用的是拷贝构造而非移动构造函数,因此在创建希望能移动对象时,不要声明它们为const
//
class Annotation {
public:
explicit Annotation(const std::string text)
:value(std::move(text)) //“移动”text到value里;这段代码执行起来
{}
private:
std::string value;
};
// std::string 类定义
class string {
public:
string(const string& rhs); //拷贝构造函数
string(string&& rhs); //移动构造函数
};
关于左值引用和右值引用能够接受参数的范围
- 当模板参数为左值引用时,只能传递给它的一个左值的实参。
- 当模板参数为
const
左值引用时,传递给它的一个左值的实参/临时对象、字面值等(右值) - 当模板函数参数是一个右值引用时(准确来说是通用引用,形式与右值引用相同),传递给它的实参可以是任意类型
上面提到了"通用引用",通用引用本质上是特定上下文的右值引用,通过类型推导区分左右值得出上下文,并且会发生引用折叠。
发生通用引用有4种情况
- 模板实例化
auto
类型推导typedef
与别名声明的创建和使用decltype
通用引用有2个特点
- 其形式与右值引用相同,这是由于通用引用本身就是特殊场景下的右值引用
- 可接受任意类型的实参
关于第2点,实际是C++两种机制叠加的效果,一是类型推导,二是引用折叠
使用通用引用时,左值实参会被推导为左值引用,右值实参会被推导为非引用,示例如下
Widget widgetFactory(); //返回右值的函数
Widget w; //一个变量(左值)
func(w); //用左值调用func;T被推导为Widget&
func(widgetFactory()); //用右值调用func;T被推导为Widget
理解引用折叠,需要首先明确以下原则
C++中引用的引用是非法的
此原则并不难理解,如果可以声明引用的引用,那么理论上就可以做到无限引用
int x;
int& &rx = x; // 错误,不能声明引用的引用
在此原则基础上,考虑把一个左值赋值给一个通用引用,在进行类型推导后会被推导为左值引用,此时出现的就是引用的引用,例如以下示例代码
template<typename T>
void func(T&& param); //同之前一样
func(w); //用左值调用func;T被推导为Widget&
以上代码在经历类型推导后,得到的结果是void func(Widget& && param);
显然这是非法的,因此编译器会进行引用折叠
经历了类型推导和引用折叠后,对于原始传入的左值,最终形成了左值引用;对于原始传入的右值,最终形成了右值引用
Reference
- [1] 深入理解左值、右值