左值和右值

C++
Author

0warning0error

Published

April 7, 2025

C++的左值和右值是比较有理解难度的概念。每个 C++ 表达式都有一个类型,属于某个值类别。 值类别是编译器在表达式计算期间创建、复制和移动临时对象时必须遵循的规则的基础。

在C++11中,值类型有以下分类:

例子

为了方便理解, 下面可以看看一些例子:

#include <iostream>
#include <utility>

class result {
  public:
    int a;
    result() : a(0) { 
        std::cout << "result" << std::endl;
    }

    ~result() { 
        std::cout << "~result" << std::endl; 
    }
};

result && func(){
    result r;
    return std::move(r);
}

int main() {
    std::cout << "first line of main" << std::endl;
    result a;
    a; // lvalue
    result(); // prvalue
    result&& r = result(); // prvalue
    result&& r = func(); // xvalue
    std::cout << "last line of main" << std::endl;
}

我们可以看到, a 因为表达式的结果是可以获取地址的,所以a属于lvalueresult()因为计算结果是没有地址的,所以属于prvalue,而func() 返回的是快要消亡的具名对象,所以func()返回的是xvalue

右值生命周期延长规则

C++11以上标准规定,当右值引用(T&&)或const左值引用(const T&)绑定prvalue时,临时对象的生命周期会被延长至引用的作用域结束。

const std::string& s1 = "Hello";  // 临时字符串生命周期延长至s1作用域结束,内容不可变
std::string&& s2 = std::string("World");  // 临时对象生命周期延长至s2作用域结束,内容可变

C++17 中 prvalue的变化

到了C++17, prvalue被弱化成只初始化对象,不再是不能被移动的临时对象。

比如下面的代码

struct NonCopyable {
    operator int() {
        return 0;
    }
    NonCopyable() = default;
    NonCopyable(const NonCopyable &) = delete;
    NonCopyable& operator=(const NonCopyable&) = delete;
};
int main() {
    [[maybe_unused]] int i = NonCopyable{};                 //1
    [[maybe_unused]] NonCopyable v = NonCopyable{};         //2
}

在C++17以前,2是不行的,这是因为 C++11 要求 RVO 或者 NRVO 也一定是需要有复制 / 移动构造函数的。而到了C++17,2是可以的,是因为 prvalue 仅仅只是一个初始化器,不是对象,不会发生拷贝,C++编译器会直接让prvalue对象在v的原地初始化,等价于NonCopyable v{}。 在汇编层面,会在进入函数之前传递v对象的地址,进入构造函数以后原地初始化。