引言
前面两篇我们学习了左值/右值的区分和移动语义。今天进入本系列最后一篇:完美转发和引用折叠。
假设你要写一个工厂函数 makeObject,它接收一个参数,然后把这个参数原封不动地传给构造函数。问题来了:如果调用者传的是左值,构造函数应该用拷贝;如果传的是右值,构造函数应该用移动。怎么让一个函数同时支持这两种情况?
C++11 的答案是 万能引用 + std::forward 。这背后的核心机制就是引用折叠。

第一部分:万能引用
一、什么是万能引用
万能引用 = T&& + 模板参数推导 。不是所有 T&& 都是万能引用!
cpp
// 1. 模板函数 + T&& → 万能引用
template<typename T>
void func(T&& param); // param 是万能引用
// 2. auto&& → 万能引用
auto&& var = 10; // var 是万能引用
// 3. 没有类型推导的 && → 纯右值引用,不是万能引用!
void func(int&& param); // param 是右值引用(不是万能引用)
template<typename T>
void func(vector<T>&& param); // param 是右值引用(不是万能引用)
class MyClass {
template<typename T>
void func(T&& param); // param 是万能引用
};
判断法则 :T&& 必须是严格 的 T&& 形式,且 T 必须是被推导的模板参数。加了 const、volatile、或者其他修饰就不是万能引用。
cpp
template<typename T>
void func(const T&& param); // const T&& → 右值引用!不是万能引用
二、万能引用的推导规则
cpp
template<typename T>
void func(T&& param) { ... }
int x = 10;
const int cx = 20;
func(x); // 传左值 → T 推导为 int& → param 类型为 int&
func(cx); // 传左值 → T 推导为 const int& → param 类型为 const int&
func(10); // 传右值 → T 推导为 int → param 类型为 int&&
func(move(x)); // 传右值 → T 推导为 int → param 类型为 int&&
核心规律:
| 传入实参 | T 推导为 | param 的实际类型 |
|---|---|---|
左值 x |
int& |
int&(引用折叠) |
左值 cx (const) |
const int& |
const int& |
右值 10 |
int |
int&& |
右值 move(x) |
int |
int&& |
传入左值时,T 被推导为左值引用类型! 这是万能引用的关键。
第二部分:引用折叠
一、为什么需要引用折叠
cpp
template<typename T>
void func(T&& param) { ... }
int x = 10;
func(x); // 传入左值,T 推导为 int&
// 那 param 的类型是什么?
// T&& = int& && ← 引用的引用?C++ 不允许!
C++ 不允许"引用的引用" 。但模板推导可能产生这种组合。于是编译器用引用折叠规则来处理:
二、引用折叠规则
只有一条核心规则 :只要有左值引用参与,结果就是左值引用。

在万能引用中的应用:
cpp
func(x); // 传入左值 → T = int&
// T&& = int& && → 折叠为 int&
// param 是 int&(左值引用)
func(10); // 传入右值 → T = int
// T&& = int&& → 不需要折叠
// param 是 int&&(右值引用)
三、引用折叠的完整推导
| 传入实参 | T 推导为 | T&& 展开 | 折叠结果 |
|---|---|---|---|
int 左值 |
int& |
int& && |
int& |
const int 左值 |
const int& |
const int& && |
const int& |
int 右值 |
int |
int&& |
int&& |
const int 右值 |
const int |
const int&& |
const int&& |
第三部分:std::forward 完美转发
一、问题场景
cpp
template<typename T>
void wrapper(T&& arg) {
// arg 在函数内部是有名字的变量
// 有名字 = 左值!
// 直接传 arg 永远触发拷贝,不会触发移动
foo(arg); // ❌ 即使外面传的是右值,这里也是拷贝
}
为什么 arg 变成左值? 因为它在函数内部有名字。"有名字的就是左值"------这是本篇第一篇的核心法则。
二、std::forward 的原理
std::forward 能根据 T 的类型,把 arg "还原" 成原来的值类别。
cpp
template<typename T>
void wrapper(T&& arg) {
// std::forward<T>(arg)
// 如果 T 是 int&(传入左值) → forward 返回左值引用
// 如果 T 是 int (传入右值) → forward 返回右值引用
foo(std::forward<T>(arg));
}
std::forward 的简化实现:
cpp
// 转发左值版本
template<typename T>
T&& forward(typename remove_reference<T>::type& t) noexcept {
return static_cast<T&&>(t);
}
// 转发右值版本
template<typename T>
T&& forward(typename remove_reference<T>::type&& t) noexcept {
return static_cast<T&&>(t);
}
std::forward<T>(arg) 的工作流程:

三、完整示例
cpp
#include <iostream>
#include <string>
using namespace std;
// 两个重载:一个左值引用,一个右值引用
void process(const string& s) { cout << "拷贝:" << s << endl; }
void process(string&& s) { cout << "移动:" << s << endl; }
// 完美转发包装器
template<typename T>
void wrapper(T&& arg) {
// arg 在这里是左值(有名字)
// 用 forward 还原它本来的值类别
process(std::forward<T>(arg));
}
int main() {
string s = "hello";
wrapper(s); // 传入左值 → 调用拷贝:hello
wrapper(string("hi")); // 传入右值 → 调用移动:hi
wrapper(std::move(s)); // 传入右值 → 调用移动:hello
return 0;
}
第四部分:std::move vs std::forward
| 对比项 | std::move |
std::forward |
|---|---|---|
| 作用 | 无条件转成右值 | 有条件还原值类别 |
| 模板参数 | 自动推导 | 必须显式指定 <T> |
| 使用场景 | 明确要移动 | 完美转发 |
| 本质 | static_cast<T&&>() |
条件性的 static_cast<T&&>() |
cpp
// std::move:无条件转右值
string s = "hello";
string s2 = std::move(s); // s 被移动
// std::forward:有条件转发
template<typename T>
void wrapper(T&& arg) {
// T 是 int& → forward 返回左值引用
// T 是 int → forward 返回右值引用
process(std::forward<T>(arg));
}
记忆口诀 :move 是"无条件变右值",forward 是"保持原样转发"。
第五部分:emplace_back 的完美转发
vector::emplace_back 是完美转发的经典应用:
cpp
vector<string> vec;
string s = "hello";
vec.push_back(s); // 拷贝 s
vec.push_back(std::move(s)); // 移动 s
vec.push_back("world"); // 临时对象 → 移动
// emplace_back 内部用完美转发,直接在容器内构造
vec.emplace_back("direct"); // 直接在 vector 内部构造,零拷贝
emplace_back 的简化实现:
cpp
template<typename T>
class vector {
public:
template<typename... Args>
void emplace_back(Args&&... args) {
// 完美转发所有参数给 T 的构造函数
new (end_ptr) T(std::forward<Args>(args)...);
}
};
为什么 emplace_back 比 push_back 高效?

第六部分:完美转发包装器完整示例
cpp
#include <iostream>
#include <memory>
#include <string>
using namespace std;
class Person {
private:
string name;
int age;
public:
Person(const string& n, int a) : name(n), age(a) {
cout << "拷贝构造 name" << endl;
}
Person(string&& n, int a) : name(std::move(n)), age(a) {
cout << "移动构造 name" << endl;
}
void show() const {
cout << name << ", " << age << endl;
}
};
// 完美转发工厂函数(类似 make_shared)
template<typename T, typename... Args>
unique_ptr<T> make_unique_custom(Args&&... args) {
return unique_ptr<T>(new T(std::forward<Args>(args)...));
}
int main() {
string name = "张三";
// 传入左值 name → 调用 Person(const string&)
auto p1 = make_unique_custom<Person>(name, 20);
p1->show();
// 传入右值 "李四" → 调用 Person(string&&)
auto p2 = make_unique_custom<Person>("李四", 25);
p2->show();
// 传入 move(name) → 调用 Person(string&&)
auto p3 = make_unique_custom<Person>(std::move(name), 30);
p3->show();
return 0;
}
第七部分:总结
一、核心概念关系

二、三篇总结
| 篇 | 核心内容 |
|---|---|
| 第一篇 | 左值(能取地址)vs 右值(临时/字面量) |
| 第二篇 | 右值引用 T&& + 移动语义 + std::move |
| 第三篇 | 万能引用 + 引用折叠 + std::forward 完美转发 |
三、一句话记忆
万能引用 T&& 配合模板推导,传左值则 T 为 int&、传右值则 T 为 int。引用折叠保证 int& && 变为 int&。std::forward 根据 T 的类型还原参数的值类别,实现完美转发。std::move 是无条件转右值,std::forward 是有条件保持原样。