C++11- 究竟什么是右值
场景一:实现移动语义 (Move Semantics) - 最重要
场景二:实现完美转发 (Perfect Forwarding)
什么是左右值
C++11之后,表达式按值类别分为三个主类别:左值 (lvalue)、将亡值 (xvalue) 和 纯右值 (prvalue)。后两者合称为右值 (rvalue)。我们有时候会弄混淆这个值类别到底是左值还是右值,那么本文来捋一下到底什么是lvalue,什么是prvalue,以及常介于这两者之间的xvalue。
expression
/ \
glvalue rvalue
/ \ / \
lvalue xvalue prvalue
-
左值 (lvalue): 具有身份(可以获取地址)且不能被移动的表达式。
-
eg:变量名、函数名、返回左值引用的函数调用、前置自增/减表达式等。
-
int a = 10;a是左值。 -
std::cout << "Hello CSDN";std::cout是左值。
-
-
-
纯右值 (prvalue): 不具有身份(通常不能获取其地址)且可以被移动的表达式。也就是常说的“临时对象”。
-
eg:字面量(如 24,
true,nullptr)、返回非引用类型的函数调用、算术/逻辑表达式的结果、后置自增/减表达式、this指针、Lambda表达式等。-
int x = 24;24 是纯右值。 -
std::string s = get_str();如果get_str()返回std::string(而非引用),则其返回值是纯右值。 -
int res = a + b;a + b这个表达式的结果是纯右值。
-
-
-
将亡值 (xvalue): 具有身份但可以被移动的表达式。可以理解为“即将被销毁的左值”。
-
eg:返回右值引用的函数调用(最典型的就是
std::move的结果)。-
std::string str = std::move(other_str);中的std::move(other_str)表达式的结果就是将亡值。
-
-
有一个关键的规则常常导致混淆:当一个纯右值被用来初始化一个对象时(具体来说,是作为初始化器),会发生“临时量实质化”(Temporary Materialization)。此时,这个纯右值会转换成一个将亡值(xvalue)。
这个将亡值是一个临时对象,它拥有身份(可以取地址),并且是右值。
#include <iostream>
void foo(const std::string& s) { // 接受常左值引用
std::cout << s << std::endl;
}
void foo(std::string&& s) { // 接受右值引用
std::cout << s << " (rvalue)" << std::endl;
}
int main() {
foo("Hello"); // 调用哪个 foo?
// 1. “Hello” 是 const char[6] 类型的字面量,它是一个纯右值 (prvalue)。
// 2. 为了匹配 foo 的参数(需要 std::string),需要构造一个临时的 std::string 对象。
// 3. 此时发生“临时量实质化”:纯右值 “Hello” 转换成了一个 std::string 类型的临时对象。
// 4. 这个临时对象是一个将亡值 (xvalue),属于右值 (rvalue)。
// 5. 因此,重载决议会选择 void foo(std::string&& s) 这个版本。
return 0;
}
输出结果将是:
Hello (rvalue)
在这个例子中,我们通常会说“传递了一个临时对象给 foo”,这个临时对象是右值。它的前身是纯右值 "Hello",但在函数传参这个“初始化”的语境下,它被实质化为了一个右值(将亡值)临时对象。
那么有没有一些方式来判断它是什么类型的值呢?
它能否放在赋值运算符左边?
左值可以:
a = 10;(正确)右值不行:
get_str() = “new value”;(错误,get_str()返回临时对象,是右值)
它能否取地址?
左值可以:
int* p = &a;(正确)实质化后的临时对象也可以取地址,但它是右值。这是一个特例。
纯右值不行:
int* p = &(a + b);(错误,a + b的结果是纯右值,无地址)
所以怎么判断是什么?
如果有名字(变量、函数名),通常是左值。
如果是字面量(
10,nullptr)、匿名对象(MyValue{})或运算结果(a+b),通常是纯右值。如果是
std::move()的结果,则是将亡值。
左右值关系
在了解完左右值到底是什么后,再来探讨一下左值和右值之间的关系,以及C++11引入的一个关键特性--右值引用的核心应用场景。
对左右值来说,最核心的区别就在于“资源的所有权”。
| 特性 | 左值 (lvalue) | 右值 (rvalue - prvalue/xvalue) |
|---|---|---|
| 身份 (Identity) | 有:可以获取地址 | prvalue无,xvalue有 |
| 可移动性 (Movable) | 否:通常不应被“窃取”资源 | 是:核心特性,资源可以被安全地“移动” |
| 比喻 | 一个有名有姓的人,他有稳定的工作和住所(内存地址)。你不能随便拿走他的东西。 | 一个匿名旅客(std::move)。他的行李可以被打包带走。 |
| 生命周期 | 通常由作用域决定(如局部变量)或手动管理(如堆内存)。 | 通常是短暂的,表达式结束后就销毁。 |
| 主要用途 | 作为数据源,提供数据。 | 作为数据源,并且可以交出资源的所有权。 |
左值可以“变身”为右值:
通过
std::move,它不做任何实际的移动操作,只是将一个左值无条件地转换为一个右值引用(xvalue)。这说明了:“我不再需要这个对象里的资源了,你可以拿走”。比如
std::string new_str = std::move(old_str)之后,old_str处于有效但未定义的状态(通常为空),它的内容被“移动”到了new_str。
所有右值都可以初始化给
const左值引用。这是C++98就有的规则。常左值引用是个“万能”的引用类型,能接收左值、常左值和右值。它延长了右值的生命周期,但无法修改其值。比如
void test(const std::string& s)可以接受变量(左值)和临时字符串(右值)。
右值引用的应用场景
右值引用(T&&)的引入,主要是为了解决不必要的深拷贝性能问题,实现了“移动语义”和“完美转发”。
场景一:实现移动语义 (Move Semantics) - 最重要
目的: 避免代价过大的深拷贝,通过“资源窃取”来高效地初始化或赋值。
class MyString {
private:
char* m_data;
size_t m_size;
public:
// 移动构造函数 (参数是右值引用)
MyString(MyString&& other) noexcept
: m_data(other.m_data), m_size(other.m_size) // 1. 窃取资源
{
other.m_data = nullptr; // 2. 置空源对象,防止其析构时释放资源
other.m_size = 0;
std::cout << "Move Constructor called\n";
}
// 移动赋值运算符
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] m_data; // 释放自己的旧资源
m_data = other.m_data; // 窃取新资源
m_size = other.m_size;
other.m_data = nullptr; // 置空源对象
other.m_size = 0;
}
std::cout << "Move Assignment called\n";
return *this;
}
// ...
};
int main() {
MyString s1("Hello"); // 普通构造函数
// 关键场景1:用函数返回的临时对象(右值)初始化新对象
MyString s2 = create_string(); // 调用移动构造,而非拷贝构造
// 关键场景2:明确表示不再需要s1,将其资源转移给s3
MyString s3 = std::move(s1); // 调用移动构造,s1现在为空
// 关键场景3:在容器操作中
std::vector<MyString> vec;
vec.push_back(MyString("World")); // push_back的重载版本接受右值,调用移动构造
return 0;
}
场景二:实现完美转发 (Perfect Forwarding)
目的: 在模板函数中,将一个参数的原生值类别(左值性或右值性)毫发无损地转发给另一个函数。
技术: 使用通用引用 (Universal Reference) T&& 和 std::forward。
std::forward 在转发时判断:如果原始参数是左值,就转发为左值;如果是右值,就转发为右值。
// 一个工厂函数模板,它将参数完美地转发给T的构造函数
template<typename T, typename... Args>
T create(Args&&... args) { // ...args 是通用引用包,能保持每个参数的值类别
return T(std::forward<Args>(args)...); // std::forward 有条件地转换回左值或右值
}
class Widget {
public:
Widget(int& i) { std::cout << "ctor (lvalue)\n"; }
Widget(int&& i) { std::cout << "ctor (rvalue)\n"; }
};
int main() {
int x = 10;
Widget w1 = create<Widget>(x); // 转发左值,调用Widget(int&)
Widget w2 = create<Widget>(20); // 转发右值,调用Widget(int&&)
// 如果没有std::forward,create内部的args都会变成左值,永远只会调用Widget(int&)
return 0;
}
想必一定会有疑惑?为什么没有 std::forward,args 都会变成左值?要理解它,需要明白两点:通用引用的推导规则和引用折叠。
在我们的模板函数中:
template<typename T, typename... Args>
T create(Args&&... args) { // 注意:这里是 Args&&, 每个参数都是通用引用
// ...
}
当调用 create<Widget>(x, 20) 时:
-
x是一个左值 (int变量),所以Args中的第一个类型被推导为int&。-
Args包的第一个元素:Arg1 = int& -
参数类型变为:
Arg1&&=>int& &&
-
-
20是一个右值 (int字面量),所以Args中的第二个类型被推导为int。-
Args包的第二个元素:Arg2 = int -
参数类型变为:
Arg2&&=>int &&
-
引用折叠 (Reference Collapsing)
C++不允许直接出现“引用的引用”(如 int& &)。为了解决这个问题,引入了引用折叠规则:
& &->&
& &&->&
&& &->&
&& &&->&&
-
那么对于第一个参数
x(左值):-
推导出的函数参数类型是
int& && -
根据规则
& &&->&,所以最终类型是int&。这是一个左值引用。
-
-
对于第二个参数
20(右值):-
推导出的函数参数类型是
int && -
没有发生折叠,所以最终类型是
int&&。这是一个右值引用。
-
有意思的地方来了:在函数内部,所有具名的变量,无论其类型是左值引用还是右值引用,它本身都是一个左值
T create(Args&&... args) { // args有名字,是“args”
// 在这里,args 是一个参数包,但包里的每个参数都有自己的名字
// 例如,你可以通过 args... 访问到两个参数,我们称它们为 arg1 和 arg2
// arg1 的类型是 int& (左值引用),但它有自己的标识符,所以它是一个左值。
// arg2 的类型是 int&& (右值引用),但它同样有自己的标识符,所以它也被当作一个左值
// 这就是问题所在:一旦你给某个东西起了名字,它就变成了左值。
}
所以这就是关键所在,如果不使用forward
那么原来的返回结果可以看成:return T(arg1, arg2); // arg1 和 arg2 现在都是左值表达式
因此,Widget 的构造函数只会看到两个左值,会永远选择 Widget(int& i) 这个版本,即使我们传入了一个右值 20。
而std::forward 是一个有条件转换的工具。它的作用是根据原始类型信息,决定是保持当前值类别,还是将其“恢复”。
有一个形象的比喻:
-
arg2(类型int&&)就像一个装着右值的盒子,但这个盒子本身有名字。当你把“盒子”递给别人(target(arg2)),别人看到的是这个“盒子”(左值)。 -
std::forward<int>(arg2)就像是把盒子里的东西(右值)倒出来,直接递给别人。现在别人看到的就是里面的东西本身(右值)。
std::forward<int&>(arg1):
因为模板参数是
int&(一个左值引用),std::forward知道arg1原本应该对应一个左值。
std::forward返回一个左值引用,保持了arg1的左值性。
std::forward<int>(arg2):
因为模板参数是
int(注意,不是int&&,这是std::forward的巧妙设计),std::forward知道arg2原本应该对应一个右值。
std::forward会对arg2进行static_cast,产生一个类型为右值引用 (int&&)、值类别为右值(具体是将亡值)的新表达式
场景三:优化容器和标准库
标准库中的容器(vector, string, map 等)都全面实现了移动语义。
-
vector::push_back(T&& value): 提供了接受右值引用的重载版本。 -
容器扩容: 当
vector需要重新分配内存时,会使用移动构造将旧元素转移到新内存,而不是拷贝构造,性能提升巨大。
总的来说,右值和右值引用不是一种孤立的语法,而是C++为了追求效率而引入的一套完整的资源管理新方式。
更多推荐
所有评论(0)