【C++】左值和右值,左值引用和右值引用
左值和右值,左值引用和右值引用
1.1 三类核心值类别
lvalue(左值)
特征:有名字,通常可以取地址、可重复引用。
典型例子:变量名、解引用表达式。
int x = 1;
int& r = x; // x 是 lvalue
int* p = &x; // &x 合法
prvalue(纯右值)
特征:纯右值,通常是临时值,不保证具有可持久身份。
典型例子:字面量、临时对象、某些按值返回。
int y = 1 + 2; // (1 + 2) 是 prvalue
std::string s = "hi"; // "hi" 转换后形成临时 prvalue
xvalue(将亡值)(C++ 11)
特征:将亡值,有身份,但资源可被“接管”。
典型例子:std::move(x) 的结果、某些返回右值引用的表达式(如返回T&&类型的函数)。
std::string a = "abc";
std::string b = std::move(a); // std::move(a) 是 xvalue
2. std::move 不移动:它只是把左值变成右值(将亡值)
这句话是理解移动语义的支点。
-
std::move(x)做的是类型转换:把x转成T&&,令表达式呈现 xvalue 性质。 -
真正的移动发生在:移动构造/移动赋值被选择并执行时。
这也解释了为什么在转发场景不能乱用 move:它会把原本的 lvalue 也强行当成可被移动的来源。
3. 引用折叠:把“转发引用”变成可计算规则
3.1 折叠发生在何处
当类型推导、typedef/using、模板实例化等导致“引用的引用”出现时,编译器会进行引用折叠。
例如:
template<class T>
void f(T&& x); // T 发生推导,可能形成引用叠加
3.2 引用折叠四条规则(必须熟练)
-
T& & -> T& -
T& && -> T& -
T&& & -> T& -
T&& && -> T&&
结论(更易记):只要出现 &,最终几乎都会折叠成 &(唯一例外是 && && 仍为 &&)。
万能引用 T&&
何时 T&& 是“转发引用”
只有当满足:
-
形如
T&& -
且
T发生类型推导(模板参数或auto推导)
它才是转发引用。
典型:
template<class T>
void wrapper(T&& x); // x 是转发引用
4.2 推导演算:左值与右值分别会怎样
template<class T>
void wrapper(T&& x);
int a = 0;
wrapper(a); // a 是 lvalue
wrapper(0); // 0 是 prvalue
-
当传入 lvalue
a:-
T推导为int& -
形参类型
T&&变成int& && -
引用折叠:
int& && -> int& -
所以
x实际类型为int&
-
-
当传入 prvalue
0:-
T推导为int -
形参类型
T&&变成int&& -
不需要折叠
-
所以
x为int&&
-
这就是“同一个 T&&,既能接左值又能接右值”的根本原因。
5. std::forward:正确保留值类别
在转发引用中,必须使用 std::forward<T>(x) 来保持来者的值类别:
#include <utility>
template<class T>
void wrapper(T&& x) {
foo(std::forward<T>(x));
}
解释(关键点):
-
若
T是U&(来自左值),forward<T>(x)返回U& -
若
T是U(来自右值),forward<T>(x)返回U&&
因此 forward 是“条件性的 move”,而不是无条件地把一切都移动掉。
6. 与 auto / decltype 的连接:推导规则如何映射到值类别
你今天学值类别,下一步最自然的连接点就是推导规则,因为它们是同一套“表达式语义”在不同位置的体现。
6.1 auto:默认按值推导(会丢引用与顶层 const)
int x = 0;
int& rx = x;
auto a = rx; // a 是 int(引用被丢弃)
auto& b = rx; // b 是 int&
6.2 decltype:看表达式的“形态”(括号决定一切)
int x = 0;
decltype(x) a = x; // int(因为 x 是名字)
decltype((x)) b = x; // int&(因为 (x) 是 lvalue 表达式)
你可以把它当作:
-
decltype(name):读“声明类型” -
decltype((expr)):读“值类别”(lvalue ->T&,xvalue ->T&&)
6.3 decltype(auto):返回类型保真,但最容易踩悬空引用
decltype(auto) f() {
int x = 1;
return (x); // 返回 int& —— 但 x 马上销毁,悬空引用(严重错误)
}
因此实践原则:
-
decltype(auto)常用于“完美返回”(保留引用语义),但必须确认返回引用指向的对象生命周期足够长。 -
如果你只是要返回一个值,优先
auto或显式类型,降低风险。
评论
发表评论