C++ 中有左值引用和右值引用。左值引用可以绑定到左值,右值引用可以绑定到右值。那有没有一种引用,既可以绑定左值又可以绑定右值呢?有的,那就是我们这篇文章的主角,万能引用。万能引用在英文中称为 Universal Reference。它的形式为 T&&,并且这里的 T 是需要进行推导的类型。

如果读者感兴趣的话,可以看一下 Scott Mayers 的这篇博文。他讲得特别得详细,建议读者都看一下。本文基本上就是基于 Scott 的这篇博文进行了精简和整理。

万能引用在代码中大概是这样出现的:

template <typename T>
void foo(T &&x) {}

auto &&x = ..;

decltype(bar) &&x = ..;

万能引用中的型别推导

a) auto, template

在模板和 auto 型别推导形式下的万能引用,T 型别的左值被推导为 T&,而 T 型别的右值被推导为 T。

template <typename T>
void foo(T &&x) {}

int x = 1;
foo(x); // T deduces to be int&
foo(1); // T deduces to be int

上面的代码中使用了引用折叠的规则,对此尚不清楚的读者可以参考我的另一篇文章

这条规则有一条隐藏的含义,那就是初始化值的引用类型在推导过程中被忽略了。不论初始化值是左值引用还是右值引用,或者是不是引用,都不影响推导的结果。非引用的左值,左值引用和右值引用都是左值,所以推导得到的型别 T 都是 T&;而只有右值会被推导为 T。

根据这条规则和引用折叠的规则,我们可以得到一条推论: 如果初始化值为左值,那么万能引用实例化为左值引用;如果初始化值为右值,那么万能引用实例化为右值引用。

int x = 1;
auto &&y = x; // 因为 x 是左值,所以 y 的类型为 T& &&,折叠为 T& 即左值引用

auto &&p = y; // 同上,y 是左值

auto &&z = 1; 初始化值为右值,所以 z 的类型为 T &&,即右值引用

b) decltype

在 decltype 推导形式下的万能引用,初始化值的引用性得以保留。也就是说,如果初始化值为非引用左值,那么推导得到的型别为 T (而非 T&);如果初始化值为左值引用,那么推导得到的型别为 T&;如果初始化值为右值引用,那么推导得到的型别为 T&&。根据这条规则和引用折叠的规则,我们可以总结出这种形式下的万能引用的场景:

int a = 1;   // a 的类型是 int
int &b = a;  // b 的类型是 int&
int &&c = 1; // c 的类型是 int&&

decltype(a) &&x = 1; // x 的类型为 int &&,即右值引用
decltype(b) &&y = a; // y 的类型是 int& && -> 折叠为 int& 即左值引用
decltype(c) &&z = 1; // z 的类型是 int&& && -> 折叠为 int&& 即右值引用

让我们在 Compiler Explorer 中验证以上的推论。

必须得是 T&& 形式

需要强调一下,任何近似 T&& 但是不完全是 T&& 的形式都有可能(没有详尽研究过,不敢轻易下绝对的断言)不是万能引用。比方说 const T&& 就不是万能引用,我们在 compiler explorer 中验证一下,在这个例子中,foo 的入参因为 const 的存在,不是万能引用,而是一个常量右值引用,因此第11行传入左值入参的时候编译报错了。

容器的陷阱

当容器的元素类型是已知的时候,容器类的成员函数的元素入参即使形如 T&&,那也不是万能引用。其实这也比较好理解,因为我们在开篇的时候就讲过,万能引用中的 T 是需要进行型别推导的,而当容器的类型是已知的时候,T 已经不需要进行型别推导了。比方说 std::vector 的 push_back 函数。

万能引用的作用

我们开篇提到,万能引用可以绑定到左值也可以绑定到右值,从逆向角度来理解的话,万能引用可以把左值映射到左值引用类型,把右值映射到右值引用类型。这个话听起来像是绕口令,不过在我们介绍完美转发之后,读者可以对此有更生动的理解。