需要预先了解:


1. 转发的问题

写一个包装函数(wrapper)时,你常常希望它把参数"原样"转交给另一个函数——参数的类型、值类别(左值/右值)、const 属性都保持不变。这件事比听起来难,先看直接的做法有什么问题。

void target(int& x)  { /* 左值版本 */ }
void target(int&& x) { /* 右值版本 */ }

template <typename T>
void wrapper(T arg)          // 按值传递
{
    target(arg);
}

按值传递有两个问题:

  1. 参数被拷贝了一份(右值时走移动构造,但左值时一定是深拷贝),大对象开销明显
  2. 无论调用者传的是左值还是右值,arg 在函数内部都是一个有名字的左值——target(arg) 永远调用左值版本,值类别信息丢失

改用引用传递呢?T& 只能绑定左值,const T& 虽然能绑定右值,但把 const 也带进去了,而且函数内部拿到的仍然是左值。普通的引用写法无法同时保留"类型"和"值类别"。


2. 万能引用

模板参数写成 T&& 时,情况完全不同:

template <typename T>
void wrapper(T&& arg);

这里的 T&& 不是普通的右值引用,而是万能引用(universal reference,标准术语叫 forwarding reference,转发引用)。它的推导规则取决于实参的值类别:

实参 T 推导为 arg 的实际类型
左值(如 int x) int& int&(折叠结果)
右值(如 10) int int&&
int x = 10;
wrapper(x);      // T = int&,arg 是 int&,绑定左值
wrapper(10);     // T = int,arg 是 int&&,绑定右值

关键在于:同一个模板可以同时接受左值和右值,并且把实参的值类别"编码"进了 T 里——左值对应 T = 类型&,右值对应 T = 类型。

注:判断是不是万能引用,看 T&& 中的 T 是否由推导决定。template <typename T> void f(T&&) 是万能引用;void f(int&&)、template <typename T> void f(const T&&)、std::vector<int>&& 都只是普通的右值引用。auto&& 也是万能引用(C++14 泛型 lambda 就靠它)。


2.1 引用折叠

为什么 T = int& 时 arg 能是 int&?因为 T&& 替换为 int& && 后,按引用折叠(reference collapsing)规则化简:

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

规则一句话:只要有一个左值引用参与,结果就是左值引用;两个都是右值引用,结果才是右值引用。


3. std::forward


3.1 命名后的参数是左值

万能引用解决了"接收"的问题,但还没解决"转交"的问题。函数体内的 arg 有名字,有名字的表达式是左值——即使它绑定的是一个右值:

template <typename T>
void wrapper(T&& arg)
{
    target(arg);    // 无论 arg 绑定什么,arg 本身是左值,永远调用左值版本
}

实测对比(consume 区分左值/右值重载):

wrapper_fwd(Probe());      // 传右值 + std::forward -> "接收右值"
wrapper_plain(Probe());    // 传右值但不 forward  -> "接收左值"(值类别丢失)

所以转交时必须手动"恢复"值类别,这就是 std::forward 的工作。


3.2 std::forward 的实现

std::forward 的原型(C++14 起)是:

template <typename T>
constexpr T&& forward(std::remove_reference_t<T>& t) noexcept
{
    return static_cast<T&&>(t);
}

它本身不做任何操作,只做一次类型转换,效果取决于模板参数 T:

  • T = int& 时,T&& 折叠为 int& —— 返回左值引用,恢复左值性
  • T = int 时,T&& 就是 int&& —— 返回右值引用,恢复右值性

还记得万能引用的推导吗?wrapper 里的 T 恰好编码了实参的值类别,把它原样传给 std::forward<T>,值类别就恢复得刚刚好:

template <typename T>
void wrapper(T&& arg)
{
    target(std::forward<T>(arg));   // T 是 int& -> 传左值;T 是 int -> 传右值
}

为什么必须显式写 <T>? forward 的参数是 remove_reference_t<T>&——如果让编译器自己推导,T 永远被推导为左值引用类型,forward 就退化成"永远返回左值引用",失去意义。显式指定是它正常工作的前提。


3.3 完整验证

#include <cstdio>
#include <utility>

struct Probe
{
    Probe() { printf("  默认构造\n"); }
    Probe(const Probe&) { printf("  拷贝构造\n"); }
    Probe(Probe&&) noexcept { printf("  移动构造\n"); }
};

void consume(Probe& p)  { printf("  接收左值\n"); }
void consume(Probe&& p) { printf("  接收右值\n"); }

// 用 std::forward 完美转发
template <typename T>
void wrapper_fwd(T&& arg)
{
    consume(std::forward<T>(arg));
}

// 不用 forward,直接传
template <typename T>
void wrapper_plain(T&& arg)
{
    consume(arg);
}

int main()
{
    Probe p;
    printf("wrapper_fwd(p): 传入左值 ->\n");          wrapper_fwd(p);
    printf("wrapper_fwd(Probe()): 传入右值 ->\n");    wrapper_fwd(Probe());
    printf("wrapper_plain(p): 直接传左值 ->\n");      wrapper_plain(p);
    printf("wrapper_plain(Probe()): 直接传右值 ->\n"); wrapper_plain(Probe());
    return 0;
}

编译并运行代码后,看到这样的输出:

  默认构造
wrapper_fwd(p): 传入左值 ->
  接收左值
wrapper_fwd(Probe()): 传入右值 ->
  默认构造
  接收右值
wrapper_plain(p): 直接传左值 ->
  接收左值
wrapper_plain(Probe()): 直接传右值 ->
  默认构造
  接收左值

wrapper_fwd 完整保留了调用者的意图:左值传左值、右值传右值。wrapper_plain 中右值退化为左值——如果 consume 内部用参数拷贝构造一个大对象,就会白白多一次深拷贝。

注:"完美"指的是值类别的保持:std::forward 不会把左值变成右值,也不会把右值变成左值,它只是把 T 里已经记录的信息翻译回引用类型。


4. std::forward 与 std::move

两个函数长得几乎一样(都是 static_cast<T&&>),但用途完全不同:

std::move std::forward<T>
行为 无条件转成右值引用 按 T 恢复值类别
模板参数 自动推导,无需指定 必须显式指定
使用场景 明确表示"这个对象我不要了",触发移动 泛型代码中转发参数,保持调用者的意图
std::string s = "hello";
consume(std::move(s));              // 无论 s 原本是什么,这里强制按右值处理
consume(std::forward<std::string&>(s));  // 明确按左值转发(等价于直接传 s)

一个简单的判断方法:在模板里转发参数用 forward,在具体类型上转移所有权用 move。把 std::forward 用在非模板代码里没有意义——那时你已经知道参数是左值还是右值了。


5. 应用:工厂函数

完美转发最常见的应用是工厂函数——接收任意参数、构造目标对象。手写一个 std::make_unique 的简化版:

#include <memory>
#include <utility>

template <typename T, typename... Args>
std::unique_ptr<T> make_unique(Args&&... args)
{
    return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}

Args&&... 是万能引用参数包,std::forward<Args>(args)... 把每个参数按原来的值类别展开转交。效果:

struct Widget
{
    explicit Widget(std::string name) : m_name(std::move(name)) {}
    std::string m_name;
};

std::string s = "hello";
auto w1 = make_unique<Widget>(s);              // 左值 -> Widget 拷贝构造
auto w2 = make_unique<Widget>(std::move(s));   // 右值 -> Widget 移动构造
auto w3 = make_unique<Widget>("hello");        // 字面量 -> 直接构造

如果把 std::forward 换成直接传参,w2 和 w3 也会走拷贝路径,传入右值时白白多一次深拷贝。std::vector 的 emplace_back、std::thread 的构造函数等,内部都是同一个模式。

注意:完美转发不完美的地方——位域不能作为转发实参(无法绑定非 const 引用)、花括号初始化列表无法推导 T(要用 auto 转换或显式构造)。这些属于边角情况,遇到时知道原因即可。