本文章主要介绍 C/C++ 编码中常见的左值 (lvalue)、右值 (rvalue) 的概念和左值引用 (lvalue reference) 、右值引用 (rvalue reference) 的概念。
左值与右值
首先,如果按照字面意义来理解,左值似乎就是指等号左边的值,右值就是指等号右边的值。当然,也有人把左值理解成“有地址的值 (located value)”,表示了一个占据内存中某个可识别的位置(也就是一个地址)的对象。
在如下代码中,变量 a
和 b
就是左值,而等号右边的常数 10
和 a + 10
则是右值。这是毋庸置疑的。
0
1
int a = 10;
int b = a + 10;
确实,左值绝大多数时间在等号左边,而右值绝大多数时间在等号右边。在上面的简单赋值语句中,变量 a
和 b
在内存中有空间,而常数 10
和 a + 10
并没有空间,或者说他们存储在代码中,或者在一个临时的不可访问的空间中。
我们不可以给一个右值赋值,如果我们写 10 = a
,这毫无疑问会在等号左边报错:表达式必须是可修改的左值
;逻辑上也说不通,很明显我们不能对一个没有空间的常量赋值。
但是我们可以把一个左值赋值给另一个左值,如下代码把左值 a
赋值给变量 c
,此时变量 a
在等号右边,但是它仍然是一个左值。
0
int c = a;
通常来说,计算对象的值的语言成分,都使用右值作为参数。例如,两元加法操作符 '+'
就需要两个右值参数,并返回一个右值。在下面的代码中,不可否认的是变量 a
和 b
都是左值,但是在第 3 行中,他们为了参加加法运算,进行了隐式的左值到右值的转换,加法返回右值再赋给左值 c
。
0
1
2
int a = 10;
int b = 20;
int c = a + b;
右值不止可以是常量,表达式,还可以是函数的返回值。在下面代码中,函数返回的 int
值是一个临时的值,是一个右值,我们把返回的右值,也就是 10
存储到左值 a
中。
0
1
2
3
4
5
int GetValue() {
return 10;
}
int main() {
int a = GetValue();
}
左值引用与右值引用
很明显,我们不能对上面这个右值返回值赋值。但是当函数返回的是左值引用 int&
,注意此时要为被引用的返回值申请一个存储空间,比如使用静态变量,我们便可以给返回值赋值了。以下代码将不会报错。C++ 中函数可以返回左值的功能对实现一些重载的操作符非常重要,比如重载方括号操作符 []
,来实现一些 STL 的查找访问的操作。
0
1
2
3
4
5
6
7
int& GetValue() {
static int value = 10;
return value;
}
int main() {
int a = GetValue();
GetValue() = 20;
}
对于左值引用,考虑函数的传参。我们知道,普通的直接传参情况下,无论传入的是左值还有右值,都可以运行,传入的是右值会被用来创建一个左值。
0
1
2
3
4
5
void SetValue(int value) {}
int main() {
int a = 10;
SetValue(a); // correct, lvalue
SetValue(10); // correct, rvalue
}
但是,当我们改用左值引用时我们会发现,这时传入右值会报错:非const引用的初始值必须是左值
。如果我们使用 const
加上左值引用就可以。这也挺容易理解,毕竟右值一般是不能修改的,所以必须加上 const
,防止出现修改了右值的情况。当然,实际上编译器可能做的是创建一个临时变量然后把它赋给左值引用。
0
1
2
3
4
5
6
7
8
9
void SetValue(int& value) {}
int main() {
int a = 10;
SetValue(a); // correct, lvalue
const int& b = 10; // correct, const reference
// int tmp = 10;
// int& b = tmp;
int& c = 10; // error
SetValue(10); // error
}
所以这是我们经常看见 C++ 写的常量引用——这兼容实际的左值和临时变量的右值,避免了不必要的临时对象的创建。
0
1
2
3
4
5
void SetValue(const int& value) {}
int main() {
int a = 10;
SetValue(a); // correct, lvalue
SetValue(10); // correct, const reference of rvalue
}
那么有了这个常量引用来兼容右值,那么右值引用又是什么东西呢?
0
1
2
3
4
5
void SetValue(int&& value) {}
int main() {
int a = 10;
SetValue(a); // error, a rvalue reference cannot be bind to a lvalue
SetValue(10); // correct, rvalue
}
看上面的右值引用传参,这回变成不接受绑定左值了。那么这有什么用呢?考虑以下重载。
0
1
2
3
4
5
6
void SetValue(int&& value) { std::cout<<"int&& "<<value<<std::endl; }
void SetValue(const int& value) { std::cout<<"const int& "<<value<<std::endl; }
int main() {
int a = 10;
SetValue(a); // lvalue, "const int& 10"
SetValue(10); // rvalue, "int&& 10"
}
我们看到,就算我们使用常量引用来兼容右值,但是它还是会优先选择右值引用。这很酷,因为如果我们知道传入的是一个临时对象的话,我们就可以不必担心他们是否活着,是否完整,是否拷贝,我们可以知道它不会像左值一样在别的函数中使用——我们可以在函数中自由地偷它的资源来肆意使用,这对优化有着很大的帮助。
所以到这里为止,我们知道左值是某种存储支持的变量,而右值只是临时的值。左值引用只接受左值,常量左值引用兼容右值,右值引用只接受右值,且在重载中优先级比常量左值引用高。
C++11 中的左值与右值
C++11 对 C++98 中的右值进行了扩充。在 C++11 中,值类别分为
Glvalue
(Generalized lvalue,泛左值 ) 是一个表达式,其计算确定对象、位域或函数的标识。Prvalue
(Pure rvalue,纯右值 ) 是一个表达式,其计算初始化对象或位域,或计算运算符的操作数的值,由其出现的上下文指定。-
Xvalue
(eXpiring value,将亡值 ) 是一个glvalue
,它表示可重复使用其资源的对象或位域, (通常是因为它接近生存期) 结束。 - 左值为不是
xvalue
的glvalue
。 - 右值为
prvalue
或xvalue
。
左值具有程序可以访问的地址。 左值表达式的示例包括变量名称,包括 const
变量、数组元素、返回 lvalue
引用的函数调用、位字段、联合和类成员。
Prvalue
表达式没有可通过程序访问的地址。 Prvalue
表达式的示例包括:文本、返回非引用类型的函数调用、字面量值以及 Lambda 表达式等等,以及在表达式计算过程中创建但仅由编译器访问的临时对象。
Xvalue
表达式的地址不能再由程序访问,但可用于初始化 rvalue
引用,后者提供对表达式的访问。示例包括返回右值引用的函数调用,以及数组下标、成员和指针到数组或对象为右值引用的成员表达式。
上面是微软的文档说的。
怎么理解 Xvalue
呢?
TBD.
References
https://www.bilibili.com/video/BV1Aq4y1t73p
https://nettee.github.io/posts/2018/Understanding-lvalues-and-rvalues-in-C-and-C/
https://docs.microsoft.com/zh-cn/cpp/cpp/lvalues-and-rvalues-visual-cpp?view=msvc-170
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.