31、左值、右值详解

厨子大约 6 分钟C++C++基础编程程序厨

关键知识点

关于左值右值这一整块,有几个知识点需要掌握:

暂时无法在飞书文档外展示此内容

  1. 纯右值(prvalue):不与对象的存储位置直接关联,无法取地址,没有标识符的临时对象。
    1. 常见的纯右值包括字面量(整数常量、字符常量等),或者表达式的计算结果等。
    2. 只能出现在等号右边。
  2. 将亡值(xvalue):即将被销毁的对象,这种对象通常是通过某些操作(std::move)或者特定的表达式(返回右值引用的函数调用)产生的。
    1. 通常与右值引用相关,实现**移动语义**。
  3. 左值(lvalue):具有存储位置的对象,其实可以被获取和修改,且在表达式结束后依然存在。
    1. 可以出现在等号左边。
    2. 常见的左值包括变量、函数或数据成员的名字。
  4. 右值(rvalue):除了左值之外的值都是右值,无法取地址,也没有标识符。
    1. 只能出现在等号右边,不能赋值给其它对象。
  5. 广义左值(glvalue):能够标识对象或函数的表达式,可以是左值或者将亡值。

几者直接的关联

几者之间的关系如图:

暂时无法在飞书文档外展示此内容

  • 左值广义左值的子集,所有的左值都属于广义左值
  • 不是所有的广义左值都是左值,因为广义左值还包括将亡值
  • 右值是除了左值之外的值,即包括将亡值纯右值
  • 广义左值左值将亡值的并集。

举例说明:

int a = 10; // a是lvalue,10是prvalue
int&& rr = 10; // rr是右值引用,绑定到prvalue 10上
std::vector<int> vec1 = {1, 2, 3};
std::vector<int>&& vec2 = std::move(vec1); // vec2是右值引用,绑定到将亡值vec1上
// 此时vec1的资源已经被移动到vec2中,vec1成为了一个将亡值(但其生命周期并未立即结束,直到vec2被销毁或不再使用vec1的资源为止)

编译器行为

编译器会将具名的右值引用视为左值,将无名的右值引用视为右值。

// named-reference.cpp
#include <iostream>
using namespace std;

// A class that contains a memory resource.
class MemoryBlock
{
   // TODO: Add resources for the class here.
};

void g(const MemoryBlock&)
{
   cout << "In g(const MemoryBlock&)." << endl;
}

void g(MemoryBlock&&)
{
   cout << "In g(MemoryBlock&&)." << endl;
}

MemoryBlock&& f(MemoryBlock&& block)
{
   g(block);
   return move(block);
}

void F1() {
    g(f(MemoryBlock()));
}

void F2() {
    auto&& t = f(MemoryBlock());
    g(t);
}

int main()
{
   F1();
   std::cout << "--- \n";
   F2();
}

会输出:https://godbolt.org/z/hKfcz8oshopen in new window

In g(const MemoryBlock&).
In g(MemoryBlock&&).
--- 
In g(const MemoryBlock&).
In g(const MemoryBlock&).
img
img

左值转右值

你可以使用std::move,来将左值转成右值:

#include <iostream>
using namespace std;

// A class that contains a memory resource.
class MemoryBlock
{
   // TODO: Add resources for the class here.
};

void g(const MemoryBlock&)
{
   cout << "In g(const MemoryBlock&)." << endl;
}

void g(MemoryBlock&&)
{
   cout << "In g(MemoryBlock&&)." << endl;
}

int main()
{
   MemoryBlock block;
   g(block); // In g(const MemoryBlock&).
   g(std::move(block)); // In g(MemoryBlock&&).
}

完美转发

完美转发有个特点:允许在传递参数时保留其原始类型,如果是左值,会保留其左值类型,如果原来是右值,也会保留其右值类型。

#include <iostream>

template <class T>
void process(T &&t)
{
    std::cout << t << " is " << "rvalue\n";
}

template <class T>
void process(T &t)
{
    std::cout << t << " is " << "lvalue\n";
}

template<typename T>
void wrapper(T &&t)
{
    process(std::forward<T>(t));
}

template<typename T>
void wrapper_common(T &&t)
{
    process(t);
}

int main()
{
    // 测试右值引用
    wrapper(1); // rvalue

    // 测试左值引用
    int i = 1;
    wrapper(i); // lvalue

    // 测试完美转发将亡值
    wrapper(std::move(i)); // rvalue

    int j = 2;
    // 测试不用完美转发
    wrapper_common(std::move(j)); // lvalue
    return 0;
}

代码示例

我们可以通过以下代码深入理解(https://godbolt.org/z/P4P731Evf):open in new window

#include <iostream>
#include <utility> // For std::move

template<typename T>
struct value_category {
    static constexpr auto value = "prvalue";
};

template<typename T>
struct value_category<T&> {
    static constexpr auto value = "lvalue";
};

template<typename T>
struct value_category<T&&> {
    static constexpr auto value = "xvalue";
};

#define VALUE_CATEGORY(expr) value_category<decltype((expr))>::value

int main() {
    int a = 5;
    int& b = a;
    int&& c = std::move(a);

    std::cout << "a is an " << VALUE_CATEGORY(a) << std::endl; // 应该是 lvalue
    std::cout << "b is an " << VALUE_CATEGORY(b) << std::endl; // 应该是 lvalue
    std::cout << "c is an " << VALUE_CATEGORY(c) << std::endl; // c是一个右值引用,但作为表达式c是lvalue
    std::cout << "std::move(a) is an " << VALUE_CATEGORY(std::move(a)) << std::endl; // 应该是 xvalue
    std::cout << "5 is an " << VALUE_CATEGORY(5) << std::endl; // 应该是 prvalue
    std::cout << "std::move(c) is an " << VALUE_CATEGORY(std::move(c)) << std::endl; // 将c转换为xvalue
    std::cout << "a + b is an " << VALUE_CATEGORY(a + b) << std::endl; // a + b 的结果是 prvalue

    // 测试一个临时对象
    std::cout << "std::string(\"temp\") is an " << VALUE_CATEGORY(std::string("temp")) << std::endl; // 应该是 prvalue

    // 测试函数返回值
    auto lambda = []() -> int&& { static int x = 10; return std::move(x); };
    std::cout << "lambda() is an " << VALUE_CATEGORY(lambda()) << std::endl; // lambda() 返回一个xvalue
}

使用场景

有些同学问到,还是不太理解右值,有没有通俗点的使用场景。

右值更多时候都是和移动语义搭配使用,完成转移所有权的任务。

正好最近很多同事离职,这里就拿个离职交接文档举例,员工A离职前一般都要把自己的文档交接转移给新接手的同事员工B,交接后,员工A的文档就跑到了员工B处,员工A不再有文档的访问权限,此时员工A就是个将亡值(名存实亡),已经被掏空了,对公司已经没有价值。

代码示例(https://godbolt.org/z/54h5bvnEs):open in new window

#include <iostream>

class Staff {
public:
    Staff(std::string doc) : doc_(std::move(doc)) {}

    Staff(Staff&& staff) {
        doc_ = std::move(staff.doc_);
    }

    void Print() {
        std::cout << "doc " << doc_ << "\n";
    }

private:
    std::string doc_;
};

int main() {
    Staff a("A职工的文档");
    a.Print(); // 输出:doc A职工的文档

    Staff b(std::move(a)); // A的文档交接给了B
    b.Print(); // 输出:doc A职工的文档(因为文档已经交接给了B,所以B拥有A的文档)
    a.Print(); // 输出:doc (因为已经交接,所以A已经被掏空了,所有权已转移)
    return 0;
}

标准库草案

具体可以看看标准库关于这块的介绍(PDF105页,书籍页码98页):

暂时无法在飞书文档外展示此内容

移动语义书籍推荐阅读

暂时无法在飞书文档外展示此内容

参考链接