欢迎光临
我们一直在努力

C++完美转发与引用折叠全解析

引言

前面两篇我们学习了左值/右值的区分和移动语义。今天进入本系列最后一篇:完美转发和引用折叠。

假设你要写一个工厂函数 makeObject,它接收一个参数,然后把这个参数原封不动地传给构造函数。问题来了:如果调用者传的是左值,构造函数应该用拷贝;如果传的是右值,构造函数应该用移动。怎么让一个函数同时支持这两种情况?

C++11 的答案是 万能引用 + std::forward。这背后的核心机制就是引用折叠。

第一部分:万能引用

一、什么是万能引用

万能引用 = T&& + 模板参数推导。不是所有 T&& 都是万能引用!

// 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、或者其他修饰就不是万能引用。

template<typename T>
void func(const T&& param); // const T&& → 右值引用!不是万能引用

二、万能引用的推导规则

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 被推导为左值引用类型! 这是万能引用的关键。


第二部分:引用折叠

一、为什么需要引用折叠

template<typename T>
void func(T&& param) { … }

int x = 10;
func(x); // 传入左值,T 推导为 int&
// 那 param 的类型是什么?
// T&& = int& && ← 引用的引用?C++ 不允许!

C++ 不允许"引用的引用"。但模板推导可能产生这种组合。于是编译器用引用折叠规则来处理:

二、引用折叠规则

只有一条核心规则:只要有左值引用参与,结果就是左值引用。

在万能引用中的应用:

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 完美转发

一、问题场景

template<typename T>
void wrapper(T&& arg) {
// arg 在函数内部是有名字的变量
// 有名字 = 左值!
// 直接传 arg 永远触发拷贝,不会触发移动
foo(arg); // ❌ 即使外面传的是右值,这里也是拷贝
}

为什么 arg 变成左值? 因为它在函数内部有名字。"有名字的就是左值"——这是本篇第一篇的核心法则。

二、std::forward 的原理

std::forward 能根据 T 的类型,把 arg "还原" 成原来的值类别。

template<typename T>
void wrapper(T&& arg) {
// std::forward<T>(arg)
// 如果 T 是 int&(传入左值) → forward 返回左值引用
// 如果 T 是 int (传入右值) → forward 返回右值引用
foo(std::forward<T>(arg));
}

std::forward 的简化实现:

// 转发左值版本
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) 的工作流程:

三、完整示例

#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::movestd::forward
作用 无条件转成右值 有条件还原值类别
模板参数 自动推导 必须显式指定 <T>
使用场景 明确要移动 完美转发
本质 static_cast<T&&>() 条件性的 static_cast<T&&>()

// 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 是完美转发的经典应用:

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 的简化实现:

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 高效?

第六部分:完美转发包装器完整示例

#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 是有条件保持原样。

赞(0)
未经允许不得转载:171主机测评 » C++完美转发与引用折叠全解析
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址