区分万能引用和右值引用
void f(Widget&& param); //右值引用
Widget&& var1 = Widget(); //右值引用
auto&& var2 = var1; //非右值引用
template<typename T>
void f(std::vector<T>&& param); //右值引用
template<typename T>
void f(T&& param); //非右值引用
实际上,T&&
有两个不同的含义。其中一种含义,是右值引用,它们仅仅会绑定到右值,而其主要的存在理由,在于识别出可移对象。
T&&
的另一种含义,它既可以是右值引用,也可以是左值引用。这种引用在源码里看起来像右值引用(即T&&
),但是它们也可以表现得像是左值引用(T&
)。它的二重性使它们既可以绑定到右值上(右值引用),也可以绑定到左值上(左值引用)。此外,它们还可以绑定到常量(const
)和非常量(non-const
)的对象上,以及volatile
对象或非volatile
对象,甚至绑定到那些既带有const
又带有volatile
饰词的对象。它们几乎可以绑定到万事万物。这种引用称为万能引用。
万能引用会出现在两种场景下。最常见的一种是函数模板的形参。
比如
template<typename T>
void f(T&& param);
第二个场景是auto
声明。
auto&& var2 = var1;
这两个场景的共同之处在于它们都涉及类型推导。在模板f
中,param
的类型是推导得到的;而在var2
的声明语句中,var2
的类型也是推导的。
相比之下,这些例子则不涉及类型推导。如果你看到了T&&
,却没有涉及到类型推导,那么,你看到的就是个右值引用。
void f(Widget&& param); //不涉及类型推导
//param是个右值引用
Widget&& var1 = Widget(); //没有类型推导
//var1是一个右值引用
因为万能引用首先是个引用,所以初始化是必需的。万能引用的初始化物会决定它代表的是个左值还是右值引用;如果初始化物是右值,万能引用就会对应到一个右值引用;如果初始化物是左值,万能引用就会对应到一个左值引用。对于函数形参的万能引用而言,初始化物在调用处提供;
template<typename T>
void f(T&& param); //param是个万能引用
Widget w;
f(w); //左值被传递给f
//param的类型是Widget&(即一个左值引用)
f(std::move(w)); //右值被传递给f
//param的类型是Widget&&(即一个右值引用)
若要使一个引用成为万能引用,其涉及类型推导是必要条件,但不是充分条件。引用声明的形式也必须正确无误,并且该形式被限定得很死;必须得正好形如T&&
才可以。
template<typename T>
void f(std::vector<T>&& param); //param是右值引用
当f
被调用时,类型T
被推导,但param
的类型声明的形式不是T&&
,而是std::vector<T>&&
,这就排除了param
是个万能引用的可能性。因此,param
是个右值引用,这个事实在你试图传递一个左值给f
时编译器会很乐意为你指出来。
std::vector<int> v;
f(v); //错误,不能给一个右值引用绑定一个左值
即使是一个const
饰词的存在,也足以使一个引用失去成为万能引用的资格。
template<typename T>
void f(const T&& param); //param是一个右值引用
如果你在一个模板内看到一个函数形参的类型写作T&&
,你可能会想当然的认为它肯定是个万能引用。不能想当然,因为,“位于模板内”并不能保证“一定涉及类型推导”。
考虑下面这个在std::vector
内的push_back
成员函数
template<class T, class Allocator=allocator<T>>
class vector{
public:
void push_back(T&& x);
...
};
push_back
的形参当然具备万能引用的正确形式。但在本例中,并不涉及类型推导。因为push_back
作为vector
的一部分,如果不存在特定的vector
实例,则它也无法存在,该实例的具体类型完全决定了push_back
的声明类型。即,给定:
std::vector<Widget> v;
会导致std::vector
模板实例化为如下实例
class vector<Widget, allocator<Widget>>
{
public:
void push_back(Widget&& x); //右值引用
};
现在可以看得很清楚了:push_back
未涉及任何类型推导,这个vector<T>
的push_back
函数的形参声明都是指向T
类型的右值引用类型。
作为对比,同样是在std::vector
内,和push_back
概念上相似的成员函数emplace_back
却涉及类型推导。
template<class T, class Allocator=allocator<T>>
class vector{
public:
template<class... Args>
void emplace_back(Args&&... args);
...
};
其中的类型形参Args
独立于vector
的类型形参T
,所以Args
必须在每次emplace_back
被调用时进行推导。
emplace_back
的类型形参的名字是Args
,但它仍然是一个万能引用,万能引用的形式必须是T&&
,但没必要一定要取T
这个名字。
下面的模板是个万能引用,因为其形式type&&
正确无误,并且param
的类型是推导而来的。
template<typename MyTemplateType>
void someFunc(MyTemplateType&& param);
类型为auto
的变量可以是万能引用。更准确地说,类型声明为auto&&
的变量是万能引用,因为会发生类型推导,并且肯定有正确的形式T&&
。auto
类型的万能引用不如模板函数参数中的万能引用常见,但是在C++11中常常突然出现。
要点速记
- 如果函数模板形参具备
T&&
类型,并且T
的类型是推导而来的,或如果对象使用auto&&
声明其类型,则该形参或对象就是个万能引用 - 如果类型声明并没有精确地具备
type&&
的形式,或者类型推导并没有发生,则type&&
就代表右值引用 - 若采用右值来初始化万能引用,就会得到一个右值引用;若采用左值来初始化万能引用,就会得到一个左值引用