学习定位:现代 C++ 高阶知识。完美转发主要服务泛型包装、工厂函数和库代码;普通开发先理解移动语义,再按需学习 T&&、引用折叠和 std::forward。
完美转发指在模板函数中,将参数原封不动地转发给另一个函数,保持其左值/右值属性、const 修饰符等。它依赖 万能引用(universal reference)和
std::forward实现。
1. 为什么需要完美转发
我们想编写一个工厂函数 make,它接受任意参数,然后转发给某个类的构造函数。
template<typename T, typename Arg>
T* make(Arg arg) {
return new T(arg); // 问题:arg 总是左值,即使传入右值也会被拷贝
}上面的代码无法区分参数是左值还是右值,导致不必要的拷贝。
2. 万能引用(Universal Reference)
当 T&& 出现在模板参数推导中时,它可能是左值引用或右值引用,具体取决于传入的参数类型。这种特殊的 && 称为万能引用。
template<typename T>
void wrapper(T&& arg) {
// arg 可能是左值引用或右值引用
}推导规则:
- 传入左值
A→T被推导为A&,T&&折叠为A&(左值引用) - 传入右值
A&&→T被推导为A,T&&折叠为A&&(右值引用)
引用折叠规则:
A& &→A&A& &&→A&A&& &→A&A&& &&→A&&
3. std::forward 的作用
std::forward<T>(arg) 根据 T 的类型决定返回左值还是右值引用:
- 若
T是左值引用,返回左值引用 - 若
T是非引用或右值引用,返回右值引用
template<typename T>
void wrapper(T&& arg) {
// 将 arg 按照原始类型转发
target(std::forward<T>(arg));
}4. 完美转发的完整示例
#include <iostream>
#include <utility>
void target(int& x) { std::cout << "左值引用\n"; }
void target(int&& x) { std::cout << "右值引用\n"; }
template<typename T>
void wrapper(T&& arg) {
target(std::forward<T>(arg));
}
int main() {
int a = 10;
wrapper(a); // 输出"左值引用"
wrapper(20); // 输出"右值引用"
wrapper(std::move(a)); // 输出"右值引用"
}5. 完美转发的典型应用
- 工厂函数:
std::make_unique、std::make_shared - 容器方法:
std::vector::emplace_back - 代理/包装器:如日志记录器、延迟求值
template<typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}6. 常见误区
- 万能引用只在模板推导时发生:
void foo(int&& x)是右值引用,不是万能引用。 std::forward必须与万能引用配合使用:不要对非模板参数使用std::forward。std::forward和std::move的区别:std::move无条件转为右值引用std::forward有条件地转为右值引用(仅当原始是右值时)
7. 总结
| 概念 | 核心语法 | 用途 |
|---|---|---|
| 右值引用 | T&&(非模板) | 绑定临时对象,实现移动语义 |
| 万能引用 | T&&(模板推导) | 保持参数类型,用于完美转发 |
std::move | std::move(x) | 将左值转为右值引用 |
std::forward | std::forward<T>(x) | 完美转发参数,保持左右值属性 |
完美转发让泛型代码能够零开销地传递任意参数,是构建高性能库的基石。