[TOC]

概述

文章参考:https://zhuanlan.zhihu.com/p/138210501

**左值(lvalue, left value)**,顾名思义就是赋值符号左边的值。准确来说, 左值是表达式(不一定是赋值表达式)后依然存在的持久对象。

**右值(rvalue, right value)**,右边的值,是指表达式结束后就不再存在的临时对象。

区分左值和右值是很重要的,这是使用C++11 move语义的基础。c++11标准基本上是通过举例来说明一个表达式是否是一个lvalue还是rvalue的。

1
lvalue = rvalue;

对于以上的语句,lvalue是我们要赋值的对象。表示了一个占据内存中某个可识别的位置(也就是一个地址)的对象。它是一个变量,存在于内存中,它的值可以被改变,可以被取地址。任何可以通过它的名字,指针或者引用来接触的变量都是lvalue,例如定义的某个变量和函数的参数, 对一个表达式取地址。

rvalue则是一个临时变量,不存在于内存中,存在于CPU的寄存器或者指令的立即数中(immediate number),因此我们不能改变它的值,不能取地址。它们通常是一个直接的数值,运算符返回的数值,或是函数的返回值,或者通过隐式类型转换得到的对象。也可以使用排除法来定义。一个表达式不是 左值 就是 右值 。 那么,右值是一个 表示内存中某个可识别位置的对象的表达式。

反面实例

我们在 C/C++ 编程中并不会经常用到 左值 (lvalue)右值 (rvalue) 两个术语。然而一旦遇见,又常常不清楚它们的含义。最可能出现两这个术语的地方是在编译错误或警告的信息中。例如,使用 gcc 编译以下代码时:

1
2
3
4
5
6
int foo() {return 2;}

int main() {
foo() = 2;
return 0;
}

你会得到:

1
2
test.c: In function 'main':
test.c:8:5: error: lvalue required as left operand of assignment

没错,这个例子有点夸张,不像是你能写出来的代码。不过错误信息中提到了左值 (lvalue)。另一个例子是当你用 g++ 编译以下代码:

1
2
3
int& foo() {
return 2;
}

现在错误信息是:

1
2
testcpp.cpp: In function 'int& foo()':
testcpp.cpp:5:12: error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'

同样的,错误信息中提到了术语右值 (rvalue)。那么,在 C 和 C++ 中,左值右值 到底是什么意思呢?我这篇文章将会详细解释。

举例

上面的术语定义显得有些模糊,这时候我们就需要马上看一些例子。我们假设定义并赋值了一个整形变量:

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
int a;  
a = 2; //a是左值,2是右值
a = 3; //编译通过!!! 左值可以被更改,编译通过
2 = 3; //错误!!! 右值不能被更改,错误
(a + 1) = 4; // 错误!


int b = 3;
int* pb = &b; //pb是左值,&b是右值,因为它是由取址运算符返回的值
&b = 0; //错误,右值不能被更改

// lvalues:
int i = 42;
i = 43; // ok, i is an lvalue
int* p = &i; // ok, i is an lvalue
int& foo();
foo() = 42; // ok, foo() is an lvalue
int* p1 = &foo(); // ok, foo() is an lvalue
// rvalues:
int foobar();
int j = 0;
j = foobar(); // ok, foobar() is an rvalue
int k = j + 2; // ok, j+2 is an rvalue
int* p2 = &foobar(); // error, cannot take the address of an rvalue
j = 42; // ok, 42 is an rvalue

那么,函数是不是就只可以作为右值呢?其实不是。考虑一个我们司空见惯的例子:

1
2
vector<int> vec = {1,2,3,4,5};
vec[1] = 99; // overloaded operator[]

我们看到,其实operator[]是一个函数,其返回值依然可以作为左值。

而 C++11 中为了引入强大的右值引用,将右值的概念进行了进一步的划分,分为:纯右值、将亡值。

**纯右值(prvalue, pure rvalue)**,纯粹的右值,没有标识符、不可以取地址的表达式, 要么是纯粹的字面量,例如 10, true; 要么是求值结果相当于字面量或匿名临时对象,例如 1+2。非引用返回的临时变量、运算表达式产生的临时变量、 原始字面量、Lambda 表达式都属于纯右值。

**将亡值(xvalue, expiring value)**,是 C++11 为了引入右值引用而提出的概念(因此在传统 C++中, 纯右值和右值是同一个概念),也就是即将被销毁、却能够被移动的值。

xvalue可能稍有些难以理解,我们来看这样的代码:

1
2
3
4
5
6
std::vector<int> foo() {
std::vector<int> temp = {1, 2, 3, 4};
return temp;
}

std::vector<int> v = foo();

可修改的左值

左值引用和右值引用

在明确了左值和右值的关系之后,对于左值的引用就是左值引用,而对于右值的引用就是右值引用。如果一个表达式的类型是一个lvalue reference (例如, T& 或 const T&, 等.),那这个表达式就是一个lvalue。其它情况,这个表达式就是一个rvalue。

C++11之前的引用,我们指的是左值引用(T&),即:

1
2
3
int a = 3;
int& ar = a; //正确的引用
int& aar = 3; //错误,我们需要让引用指向一个左值。因为3是一个字面量,不能取地址。

C++11引入了右值引用的概念,使我们可以取一个右值的引用(T&&):

1
2
int&& a = 3;  //正确
int&& b = MyInt(); //正确

C++11 提供了std::move这个方法将左值参数无条件的转换为右值, 有了它我们就能够方便的获得一个右值临时对象,对对象类型右值引用的转换。

1
2
3
4
5
6
7
8
9
int main()
{
static_cast<int&&>(7); // The expression static_cast<int&&>(7) belongs
// to the xvalue category, because it is a cast
// to an rvalue reference to object type.
std::move(7); // std::move(7) is equivalent to static_cast<int&&>(7).

return 0;
}

需要拿到一个将亡值,就需要用到右值引用的申明:T &&,其中T是类型。 右值引用的声明让这个临时值的生命周期得以延长、只要变量还活着,那么将亡值将继续存活。更多细节可参考:

move语义

传统的 C++ 没有区分『移动』和『拷贝』的概念,造成了大量的数据拷贝,浪费时间和空间。 右值引用的出现恰好就解决了这两个概念的混淆问题,为了结合左值引用来轻易完成move语义的实现。什么是move语义,为什么需要move语义,我们来举一个std::vector的栗子。我们在执行v2=v1时,需要先完成一次函数调用,即调用拷贝赋值运算符,然后执行内存分配,最后循环逐个元素。

如果v1和v2我们都需要的话,生成两份拷贝自然是没有问题的,但多数情况下我们只希望使用v2,那么我们就只希望生成一份拷贝,减少不必要又麻烦的拷贝过程:设想如果它包含10000个元素,要增加多大的开销?例如下面的swap函数:

1
2
3
4
5
6
7
8
9
10
11
12
template <class T> swap(T& a, T& b){
T tmp(a); //现有两份a的拷贝,tmp和a
a = b; //现有两份b的拷贝,a和b
b = tmp; //现有两份tmp的拷贝,b和tmp
}

//试试更好的方法,不会生成额外的拷贝
template <class T> swap(T& a, T& b){
T tmp(std::move(a)); //只有一份拷贝,tmp
a = std::move(b); //只有一份拷贝,a
b = std::move(tmp); //只有一份拷贝,b
}

move函数所做的只是拿到一个左值或右值参数,然后都将其返回为右值同时不触发任何拷贝函数。它的作用就是就是相当于把参数的值剪切到目标对象的值,move可以说是一种具有破坏性的读操作。

1
2
3
4
5
6
7
8
9
10
std::string str = "Hello";
std::vector<std::string> v;

v.push_back(str);
std::cout<<"After copy, str is \""<<str<<\"\n;
//输出结果为 After copy, str is "Hello"

v.push_back(std::move(str));
std::cout<<"After move, str is \""<<str<<\"\n;
//输出结果为 After move, str is ""

放在v2=v1里,我们能让参数v1的体积为0,只生成v2,避免生成额外拷贝。要实现这样移值的move语义,右值引用的好处就来了,它使我们能轻松快速地完成这个功能:

1
2
3
4
5
template <class T>
typename remove_reference<T>::type&& //去掉引用,再变为右值引用作为返回值类型
move(T&& a){
return a; //通过引用直接把右值传给目标
}

C++11也引入了使用move语义来实现的移动构造函数的概念。相比C++98的拷贝构造函数,其区别就相当于剪切粘贴和复制粘贴,就像上面的例子一样。

1
2
3
4
void someFunc(Widget w);
Widget wid;
someFunc(wid); //w是由wid拷贝构造
someFunc(std::move(wid)); //w是由wid移动构造

比如:

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
27
28
29
30
31
32
#include <iostream>
class A {
public:
int *pointer;
A():pointer(new int(1)) {
std::cout << "构造" << pointer << std::endl;
}
A(A& a):pointer(new int(*a.pointer)) {
std::cout << "拷贝" << pointer << std::endl;
} // 无意义的对象拷贝
A(A&& a):pointer(a.pointer) {
a.pointer = nullptr;
std::cout << "移动" << pointer << std::endl;
}
~A(){
std::cout << "析构" << pointer << std::endl;
delete pointer;
}
};
// 防止编译器优化
A return_rvalue(bool test) {
A a,b;
if(test) return a; // 等价于 static_cast<A&&>(a);
else return b; // 等价于 static_cast<A&&>(b);
}
int main() {
A obj = return_rvalue(false);
std::cout << "obj:" << std::endl;
std::cout << obj.pointer << std::endl;
std::cout << *obj.pointer << std::endl;
return 0;
}