【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&&

    • 不需要折叠

    • 所以 xint&&

这就是“同一个 T&&,既能接左值又能接右值”的根本原因。


5. std::forward:正确保留值类别

配合万能引用一起使用
当函数需要基于左值右值的重载时,先用万能引用将

在转发引用中,必须使用 std::forward<T>(x) 来保持来者的值类别:

#include <utility>

template<class T>
void wrapper(T&& x) {
foo(std::forward<T>(x));
}

解释(关键点):

  • TU&(来自左值),forward<T>(x) 返回 U&

  • TU(来自右值),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 或显式类型,降低风险。

评论

此博客中的热门博文

【Hybrid 引擎】从0开始的引擎开发——01 项目准备

【C++】类型限定符const