© 2021 CHEN Yuhan

浅谈左值和右值

lvalue & rvalue

Posted by lzzmm on March 13, 2021
About 10 minutes to read

本文章主要介绍 C/C++ 编码中常见的左值 (lvalue)、右值 (rvalue) 的概念和左值引用 (lvalue reference) 、右值引用 (rvalue reference) 的概念。

左值与右值

首先,如果按照字面意义来理解,左值似乎就是指等号左边的值,右值就是指等号右边的值。当然,也有人把左值理解成“有地址的值 (located value)”,表示了一个占据内存中某个可识别的位置(也就是一个地址)的对象。

在如下代码中,变量 ab 就是左值,而等号右边的常数 10a + 10 则是右值。这是毋庸置疑的。

0
1
int a = 10;
int b = a + 10;

确实,左值绝大多数时间在等号左边,而右值绝大多数时间在等号右边。在上面的简单赋值语句中,变量 ab 在内存中有空间,而常数 10a + 10 并没有空间,或者说他们存储在代码中,或者在一个临时的不可访问的空间中。

我们不可以给一个右值赋值,如果我们写 10 = a,这毫无疑问会在等号左边报错:表达式必须是可修改的左值;逻辑上也说不通,很明显我们不能对一个没有空间的常量赋值。

但是我们可以把一个左值赋值给另一个左值,如下代码把左值 a 赋值给变量 c,此时变量 a 在等号右边,但是它仍然是一个左值。

0
int c = a;

通常来说,计算对象的值的语言成分,都使用右值作为参数。例如,两元加法操作符 '+' 就需要两个右值参数,并返回一个右值。在下面的代码中,不可否认的是变量 ab 都是左值,但是在第 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,它表示可重复使用其资源的对象或位域, (通常是因为它接近生存期) 结束。

  • 左值为不是 xvalueglvalue
  • 右值为 prvaluexvalue

C++ expression value categories.

左值具有程序可以访问的地址。 左值表达式的示例包括变量名称,包括 const 变量、数组元素、返回 lvalue 引用的函数调用、位字段、联合和类成员。

Prvalue 表达式没有可通过程序访问的地址。 Prvalue 表达式的示例包括:文本、返回非引用类型的函数调用、字面量值以及 Lambda 表达式等等,以及在表达式计算过程中创建但仅由编译器访问的临时对象。

Xvalue 表达式的地址不能再由程序访问,但可用于初始化 rvalue 引用,后者提供对表达式的访问。示例包括返回右值引用的函数调用,以及数组下标、成员和指针到数组或对象为右值引用的成员表达式。

上面是微软的文档说的。

怎么理解 Xvalue 呢?

TBD.

References

https://www.bilibili.com/video/BV1Aq4y1t73p

https://www.yhspy.com/2019/08/31/C-%E5%B7%A6%E5%80%BC%E3%80%81%E5%8F%B3%E5%80%BC%E4%B8%8E%E5%8F%B3%E5%80%BC%E5%BC%95%E7%94%A8/

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

Creative Commons License本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.