C++ 中的 `T&&`:右值引用与转发引用

C++ 中的 T&& 要区分两种情况:右值引用 和转发引用 。关键在于:T 是否是正在推导的模板参数。

1. 右值引用

当类型是确定的,&& 表示右值引用,通常可以绑定临时对象,或通过 std::move 转换后的对象。

cpp 复制代码
int a = 10;

int&& r1 = 42;           // 可以:42 是右值
int&& r2 = a;            // 错误:a 是左值
int&& r3 = std::move(a); // 可以

可以粗略理解为:

  • 左值 :有身份、可被持续引用的对象,例如变量 a。
  • 右值:包括临时值(纯右值),以及被标记为"可移动"的对象表达式(将亡值)。

注意:std::move 本身不移动任何数据,只是把表达式转换为右值类别。真正的移动由后续的移动构造或移动赋值完成。

主要用途:移动语义

cpp 复制代码
class Buffer {
public:
    Buffer(Buffer&& other) noexcept;            // 移动构造
    Buffer& operator=(Buffer&& other) noexcept; // 移动赋值
};

移动操作可以接管另一个对象持有的资源,避免昂贵的深拷贝。

cpp 复制代码
std::string a = "hello";
std::string b = std::move(a);

此后 a 仍是有效对象,但其内容是"有效但未指定"的状态:不应假定内容保持不变,也不保证为空。

2. 转发引用:参与推导的 T&&

在下面的函数模板中,T&& 是转发引用(也叫万能引用),既能接收左值,也能接收右值:

cpp 复制代码
template<class T>
void f(T&& x);

调用时:

cpp 复制代码
int a = 10;

f(a);  // T 推导为 int&,参数类型最终为 int&
f(10); // T 推导为 int,参数类型最终为 int&&

引用折叠

上面的结果来自引用折叠规则:

组合 折叠结果
T& & T&
T& && T&
T&& & T&
T&& && T&&

口诀:只要有一个 &,结果就是 &;只有两个 && 才是 &&。

这些组合通常通过模板替换、类型别名或 decltype 形成,不能直接写成 int& &&。

不是所有模板里的 T&& 都是转发引用

cpp 复制代码
template<class T>
void f(const T&& x); // 右值引用:带 const,不是转发引用

template<class T>
void f(std::vector<T>&& x); // 右值引用:形式不是 T&&

template<class T>
struct Box {
    void g(T&& x);   // 右值引用:T 在 Box 实例化时就已确定
};

转发引用必须同时满足:形式恰好是 T&&(不能带 const),并且 T 是当前函数模板调用中参与推导的模板类型参数。

另一个常见的转发引用是 auto&&:

cpp 复制代码
int a = 10;

auto&& x = a;  // int&
auto&& y = 10; // int&&

3. 容易忽略:有名字的右值引用,其表达式是左值

cpp 复制代码
void consume(int& x)  { /* ... */ }
void consume(int&& x) { /* ... */ }

void test(int&& x) {
    consume(x);            // 调用 consume(int&)
    consume(std::move(x)); // 调用 consume(int&&)
}

虽然变量 x 的类型 是 int&&,但表达式 x 是左值(因为它有名字,可以被反复使用)。

声明类型和表达式的值类别不是一回事。

4. 完美转发:搭配 std::forward

如果要把参数继续传给别的函数,同时保留调用者传入时的左值/右值属性,就用 std::forward:

cpp 复制代码
#include <utility>

void consume(int&)  { /* ... */ }
void consume(int&&) { /* ... */ }

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

int main() {
    int a = 10;
    wrapper(a);  // 最终调用 consume(int&)
    wrapper(10); // 最终调用 consume(int&&)
}

两者的区别:

  • std::move(x):无条件转换为右值类别。
  • std::forward<T>(x):根据推导出的 T,有条件地保留原始值类别。

总结

确定类型的 T&& 是右值引用;参与模板推导的 T&& 可能是转发引用。移动用 std::move,完美转发用 std::forward。

复制代码
**修改说明:**

1. **格式**:把终端里的方框表格改成标准 Markdown 表格,加了标题层级和代码块语言标记。
2. **右值的定义**:原文"标记为可移动的对象表达式"没问题,补充了标准术语(纯右值、将亡值),方便读者对应到规范。
3. **"有效但未指定"**:移动后对象的状态,补上了标准里的说法。
4. **`const T&&` 的说明**:原文只写"右值引用",补了原因(带 `const` 就不满足转发引用的形式)。
5. **新增 `std::vector<T>&&` 示例**:这是另一个容易误判的写法。
6. **示例可编译**:`consume` 原来只有声明,没有函数体,直接编译会链接失败,补上了空函数体,并给完美转发的例子加了 `main`。
7. **细节**:删除了 `int, 参数` 中多余的空格,口径统一用"类型"和"表达式"区分声明与值类别。
相关推荐
呆呆虫121 小时前
用 orjson 加速 SerpBase 响应解析:基准对比与注意事项
开发语言·php
心疼你的一切2 小时前
Python自动发邮件实战定时发报表
大数据·开发语言·python
Wang's Blog3 小时前
Java 项目实战:交友项目 - 用户体系集成总结
java·开发语言·交友
会编程的小孩3 小时前
C++ 定时器
c++
中科三方3 小时前
IPv6检测常见问题:AAAA记录检测不通过是什么原因?
开发语言·php
沫璃染墨4 小时前
从零入门计算机网络系列(十一):HTTP协议进阶——从重定向原理到GET/POST与Fiddler抓包
linux·服务器·开发语言·网络·c++·tcp/ip·html
刚入门的大一新生4 小时前
Linux-线程互斥
java·开发语言
香芋芋圆5 小时前
iD-editor 详解|OSM官方在线编辑器,基于D3.js的轻量地图编辑器
开发语言·javascript·编辑器
揽星河..6 小时前
【C语言】分支和循环
c语言·c++·算法