为了声明指向某类型T的右值引用,你会写T&&。这会使我们在看到源码种出现“T&&”时,就认为是一个右值引用。但,没那么简单:
void f(Widget&& param); // rvalue reference
Widget&& var1 = Widget(); // rvalue reference
auto&& var2 = var1; // not rvalue reference
template<typename T>
void f(std::vector<T>&& param); // rvalue reference
template<typename T>
void f(T&& param); // not rvalue reference
事实上,“T&&” 有两种不同的涵义。当然,其中一个是右值引用的意思。这种引用行为就是你所期望的:它们只绑定到右值上去,并且它们的主要职责就是去明确一个对象是可以被move的。
“T&&” 的另一种含义是,则表示其既可以是右值引用,也可以是左值引用。带有这种含义的引用在代码中形如右值引用(即 “T&&”),但它们能表现的像左值引用一样(即 “T&”)。这种双重特性使之既可以绑定到右值(如右值引用),也可以绑定到左值(左值引用)。此外,它们也可以绑定到const对象或非const对象,以及volatile对象或非volatile对象,甚至绑定到既带有const又带有volatile属性的对象。它们几乎可以绑定到任何东西。我们称这种史无前例的灵活性的引用为万能引用【Item 25将会解释万能应用几乎总是需要应用std::forward。目前C++ 委员会的一些成员开始称其为转发引用(forwarding references)】。
万能引用出现在两种场景下。最常用的场景是函数模板参数,比如上面的代码:
template<typename T>
void f(T&& param); // param is a universal reference
另外一种场景是auto声明,比如上面代码中的:
auto&& var2 = var1; // var2 is a universal reference
这两种场景的共同之处,在于它们都涉及到类型推导。在模板f中,param的类型是推导得到的,而在var2的声明语句中,var2的类型也是推导得到的。相比之下,下面的例子就不涉及到类型推导。如果你看到没有类型推导的 “T&&” 时,那么就是右值引用:
void f(Widget&& param); // no type deduction;
// param is an rvalue reference
Widget&& var1 = Widget(); // no type deduction;
// var1 is an rvalue reference
因为万能引用也是引用,所以它必须被初始化。万能应用的初始化物(initializer)决定了它代表的是左值引用还是右值引用。如果initializer是右值,万能引用相当于右值引用,如果initializer是左值,则万能应用相当于左值引用。对于作为函数形参的万能引用而言,初始化物(initializer)在调用出提供:
template<typename T>
void f(T&& param); // param is a universal reference
Widget w;
f(w); // lvalue passed to f; param's type is
// Widget& (i.e., an lvalue reference)
f(std::move(w)); // rvalue passed to f; param's type is
// Widget&& (i.e., an rvalue reference)
要使一个引用成为万能引用,类型推导是必要非充分条件。引用声明的形式也必须正确无误,且该形式被限定的很死:必须形如 “T&&” 才行【换一种好理解的表述:如果函数模板形参具备T&&格式,且T类型是推导而来,或者对象使用auto&&声明其类型,则该形参或对象就是个万能引用】。再看一次这个我们之前在示例代码中看过的例子:
template<typename T>
void f(std::vector<T>&& param); // param is an rvalue reference
当f被调用时,类型T将被推导(除非调用者显示指明了类型,这是一种我们不必关心的边界情况),但paramr的类型声明形式不是 “T&&”,而是 “std::vector&&”。这就排除了param是万能引用的可能性。因此,param是一个右值引用,如果你尝试向f传递左值,编译器会很乐意为你确认出来:
std::vector<int> v;
f(v); // error! can't bind lvalue to
// rvalue reference
即使是一个const修饰的存在,也足以褫夺一个引用成为万能引用的资格:
template<typename T>
void f(const T&& param); // param is an rvalue reference
如果在模板内看到一个函数的形参类型写作 “T&&”,你可能想当然的认为它肯定是一个哇能引用。其实不然。因为位于模板内并不保证一定涉及类型推导,考虑如下代码:
template<class T, class Allocator = allocator<T>> // from C++
class vector { // Standards
public:
void push_back(T&& x);
…
};
push_back的形参格式虽然符合万能引用的格式,但是并不涉及类型推导。因为push_back不能存在于vector的特定实例之外,并且实例的类型就完全能决定push_back的声明类型了。也就是说:
std::vector<Widget> v;
使得std::vector模板被实例化为下面这样:
class vector<Widget, allocator<Widget>> {
public:
void push_back(Widget&& x); // rvalue reference
…
};
现在你能清楚地发现push_back没有用到类型推导。所以这个push_back是指向T的右值引用。
std::vector中还有一个与push_back概念类似的emplace_back就涉及到了类型推导:
template<class T, class Allocator = allocator<T>> // still from
class vector { // C++
public: // Standards
template <class... Args>
void emplace_back(Args&&... args);
…
};
在这里,类型参数Args独立于vector的类型参数T,所以每次emplace_back被调用的时候,Args必须被推导。(好吧,Args事实上是一个参数包,不是一个类型参数,但是为了讨论的目的,我们能把它视为一个类型参数。)
前面提到过aoto变量也可以作为万能引用。准确地说,声明为auto&&类型的变量都是万能引用,因为它们既涉及到类型推导,也有正确的格式(“T&&”)。 auto万能引用在C++ 11中没有像函数模板形参的万能引用那么常见,但是在C++ 14中却经常出现,尤其是写一个lambda表达式:
auto timeFuncInvocation =
[](auto&& func, auto&&... params) // C++14
{
// start timer;
std::forward<decltype(func)>(func)( // invoke func
std::forward<decltype(params)>(params)... // on params
);
// stop timer and record elapsed time;
};
也许你会对“std::forward<decltype(blah blah blah)>”这种形式的代码迷惑,不过没关系,Item 33中会有详细展示。
其实呢,本条款所说的万能引用是一个抽象的说法,它底层的真相被称为“引用折叠”。Item 28会专门讲这个问题。之所以本条款讨论了万能引用和右值引用的区别,目的是为了我们能更精准的阅读代码(“我看到的T&&只能绑定到右值上,还是能绑定到所有东西上呢?”),并且在你和同事讨论的时候,它能让你避免歧义。(“我在这里使用一个universal引用,不是一个右值引用…”)。它也能让你搞懂Item 25和Item 26的意思,这两个Item都依赖于这两个引用的区别。另外,掌握万能引用的概念会比了解引用折叠的技术细节是个更好的选择。
Things to Remember
- 如果函数模板形参具备T&&格式,并且T类型是推导而来,或者对象使用auto&&声明其类型,则该形参或对象就是个万能引用;
- 如果类型声明并不精确的匹配type&&格式,类型推导没有发生,type&&就代表右值引用;
- 如果采用右值初始化万能引用,就会得到一个右值引用;如果用左值初始化万能引用,就会得到一个左值引用;