完美转发与万能引用详解 | C++高级特性

已经有了万能引用,为什么还需要完美转发呢? #

引言 #

在C++中,完美转发和万能引用是两个密切相关但又容易混淆的概念。

万能引用 #

什么是万能引用? #

万能引用的形式是 T&&,应用场景:

  • T 是一个 模板参数,且需要进行类型推导时(例如函数模板参数)。
  • 当使用 auto&& 声明变量时。

此时 T&&auto&& 可以绑定到 左值、右值,因此被称为万能引用。

工作原理 #

C++规定,当引用的引用出现时,比如 T& && 或者 T&& &,会通过引用折叠简化为单一引用:

  • T& &T& (左值引用)
  • T& &&T& (左值引用)
  • T&& &T& (左值引用)
  • T&& &&T&& (右值引用)

示例代码:

template<typename T>
void foo(T&& arg) {
    // arg 是万能引用,可以绑定到左值或右值
}

int main() {
    int a = 10;
    foo(a);       // T 推导为 int& → arg 类型是 int&(左值引用)
    foo(10);      // T 推导为 int  → arg 类型是 int&&(右值引用)
}

注意:万能引用是实现完美转发的基础。

完美转发 #

完美转发允许在传递参数时保留其原始类型:

  • 如果是左值,会保留其左值类型
  • 如果是右值,会保留其右值类型
#include <iostream>

template <class T>
void process(T &&t) {
    std::cout << t << " is " << "rvalue\n";
}

template <class T>
void process(T &t) {
    std::cout << t << " is " << "lvalue\n";
}

template<typename T>
void wrapper(T &&t) {
    process(std::forward<T>(t));
}

template<typename T>
void wrapper_common(T &&t) {
    process(t);
}

int main() {
    // 测试右值引用
    wrapper(1); // rvalue
    // 测试左值引用
    int i = 1;
    wrapper(i); // lvalue
    // 测试完美转发将亡值
    wrapper(std::move(i)); // rvalue
    int j = 2;
    // 测试不用完美转发
    wrapper_common(std::move(j)); // lvalue
    return 0;
}

为什么要有完美转发? #

编译器会将 具名 的右值引用视为左值,将 无名 的右值引用视为右值。这就是为什么我们需要完美转发。

// named-reference.cpp
#include <iostream>
using namespace std;

// A class that contains a memory resource.
class MemoryBlock {
    // TODO: Add resources for the class here.
};

void g(const MemoryBlock&) {
    cout << "In g(const MemoryBlock&)." << endl;
}

void g(MemoryBlock&&) {
    cout << "In g(MemoryBlock&&)." << endl;
}

MemoryBlock&& f(MemoryBlock&& block) {
    g(block);
    return move(block);
}

void F1() {
    g(f(MemoryBlock()));
}

void F2() {
    auto&& t = f(MemoryBlock());
    g(t);
}

int main() {
    F1();
    std::cout << "--- \n";
    F2();
}

会输出:https://godbolt.org/z/hKfcz8osh

In g(const MemoryBlock&).
In g(MemoryBlock&&).
---
In g(const MemoryBlock&).
In g(const MemoryBlock&).

而我们很多时候期望的是, 只要它是右值,无论它是否具名,都应该当作右值处理,为达到这一目的,就需要用到完美转发了。

有了万能引用,为什么还需要完美转发呢? #

注意:万能引用是实现完美转发的基础。

  • 万能引用是参数绑定的 入口,负责接收任意的值类别。
  • std::forward 是转发的 出口,负责恢复原始值的类别。(如果不 forward,如果参数是个具名变量,它始终会被视为左值。)

两者缺一不可,一个是入口,一个是出口。