31、左值、右值详解
大约 6 分钟C++C++基础编程程序厨
关键知识点
关于左值右值这一整块,有几个知识点需要掌握:
暂时无法在飞书文档外展示此内容
- 纯右值(
prvalue
):不与对象的存储位置直接关联,无法取地址,没有标识符的临时对象。- 常见的纯右值包括字面量(整数常量、字符常量等),或者表达式的计算结果等。
- 只能出现在等号右边。
- 将亡值(
xvalue
):即将被销毁的对象,这种对象通常是通过某些操作(std::move
)或者特定的表达式(返回右值引用的函数调用)产生的。- 通常与右值引用相关,实现**
移动语义
**。
- 通常与右值引用相关,实现**
- 左值(lvalue):具有存储位置的对象,其实可以被获取和修改,且在表达式结束后依然存在。
- 可以出现在等号左边。
- 常见的左值包括变量、函数或数据成员的名字。
- 右值(rvalue):除了左值之外的值都是右值,无法取地址,也没有标识符。
- 只能出现在等号右边,不能赋值给其它对象。
- 广义左值(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/hKfcz8osh
In g(const MemoryBlock&).
In g(MemoryBlock&&).
---
In g(const MemoryBlock&).
In g(const MemoryBlock&).
左值转右值
你可以使用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):
#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):
#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页):
暂时无法在飞书文档外展示此内容
移动语义书籍推荐阅读
暂时无法在飞书文档外展示此内容
参考链接
- https://learn.microsoft.com/en-us/cpp/cpp/lvalues-and-rvalues-visual-cpp?view=msvc-170
- https://learn.microsoft.com/en-us/cpp/cpp/move-constructors-and-move-assignment-operators-cpp?view=msvc-170
- https://learn.microsoft.com/en-us/cpp/cpp/rvalue-reference-declarator-amp-amp?view=msvc-170
- https://en.cppreference.com/w/cpp/language/value_category
- https://stackoverflow.com/questions/3601602/what-are-rvalues-lvalues-xvalues-glvalues-and-prvalues
- https://www.geeksforgeeks.org/understanding-lvalues-prvalues-and-xvalues-in-ccwith-examples/
