学习定位:现代 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 可能是左值引用或右值引用
}

推导规则

  • 传入左值 AT 被推导为 A&T&& 折叠为 A&(左值引用)
  • 传入右值 A&&T 被推导为 AT&& 折叠为 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_uniquestd::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::forwardstd::move 的区别
    • std::move 无条件转为右值引用
    • std::forward 有条件地转为右值引用(仅当原始是右值时)

7. 总结

概念核心语法用途
右值引用T&&(非模板)绑定临时对象,实现移动语义
万能引用T&&(模板推导)保持参数类型,用于完美转发
std::movestd::move(x)将左值转为右值引用
std::forwardstd::forward<T>(x)完美转发参数,保持左右值属性

完美转发让泛型代码能够零开销地传递任意参数,是构建高性能库的基石。