T&&的两种含义
如果需要声明类型T的右值引用,我们应当撰写T&&。但当我们在某些源码中看到T&&,这并不代表它们是rvalue reference:1
2
3
4
5
6
7void 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 &&”有两种不同的含义。当其具备ravlue reference的含义时,它们只与rvalues绑定,以识别可被移动的对象。
“T&&”的另一个含义是既可作为rvalue reference亦可作为lvalue reference。这种引用看起来像右值引用,但其行为却类似于左值引用,它的双重性质允许它绑定到rvalues(如rvalue引用)以及lvalues(如左值引用)。此外,它也可以绑定到const或non-const对象,volatile对象或non-volatile对象,甚至兼具const与volatile的对象。由于这种引用几乎可以绑定一切对象,因此作者将其称为universal reference。
universal reference
universal reference出现在两种情况下。最常见的是函数模板参数,例如:1
2template<typename T>
void f(T&& param); // param is a universal reference
也可能出现在auto类型推衍之中,例如:1
auto&& var2 = var1; // var2 is a universal reference
显然,上述实例都离不开类型推衍。也就是说,如果当前场景内不存在类型推衍,那你看到的T&&必然是右值引用:1
2void f(Widget&& param); // no type deduction;param is an rvalue reference
Widget&& var1 = Widget(); // no type deduction;var1 is an rvalue reference
由于universal reference依旧是一个reference,因此必须对它们执行初始化操作,并且其初始化操作决定了其表现类型:以右值初始化则表现为右值引用,反之则为左值引用。作为函数形参的universal reference初始化操作发生于调用时:1
2
3
4
5template<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)
universal refernce的判断
对于universal reference而言,类型推衍是必要而非充分条件,引用必须被声明为“T&&”形式才能够触发universal reference,试看刚才的实例:1
2template<typename T>
void f(std::vector<T>&& param); // param is an rvalue reference
在调用f时会执行类型推衍,但是param的类型声明的形式并非“T &&”,而是“std::vector<T> &&”。 我们之前强调过,universal reference必须是“T &&”才行,因此,param是一个右值引用,如果尝试将左值传入,编译器将会报错:1
2std::vector<int> v;
f(v); // error! can't bind lvalue to rvalue reference
即使只有一个const修饰符也足以取消universal reference:1
2template<typename T>
void f(const T&& param); // param is an rvalue reference
你可能会认为模板中的参数“T &&”必然是universal reference,但事实并非如此,因为模板也并非一定触发类型推衍,考虑std::vector中的push_back成员函数:1
2
3
4
5
6template<class T, class Allocator = allocator<T>> // from C++
class vector { // Standards
public:
void push_back(T&& x);
…
};
尽管该函数中出现了“T &&”这种形式,但由于成员函数在模板实例化之后才会存在,因此在实例化之前该成员函数可视为无效,假若我们当前对该模板执行实例化:1
std::vector<Widget> v;
这直接导致该模板被实例化为:1
2
3
4
5class vector<Widget, allocator<Widget>> {
public:
void push_back(Widget&& x); // rvalue reference
…
};
显然,push_back的成员函数总是一个右值引用,根本不会触发类型推衍。
相比之下,std::vector中的emplace_back成员函数则确实触发了类型推衍:1
2
3
4
5
6
7template<class T, class Allocator = allocator<T>> // still from
class vector { // C++
public: // Standards
template <class... Args>
void emplace_back(Args&&... args);
…
};
在该声明中。Args作为一个独立于类型T的参数包,将会在实例化之中仍然执行类型推衍,因此它是一个universal reference,此外这个实例还说明了universal refernce并非一定为“T &&”,也有可能是“Type &&”:1
2template<typename MyTemplateType> // param is a
void someFunc(MyTemplateType&& param); // universal reference
universal refernce与auto
C++11和C++14中经常会出现“auto &&”,这也代表了universal reference。在C++14中,lambda可以声明auto&&参数,以一个记录函数运行执行时间的lambda为例:1
2
3
4
5auto timeFuncInvocation =[](auto&& func, auto&&... params){
start timer;
std::forward<decltype(func)>(func)(std::forward<decltype(params)>(params));
stop timer and record elapsed time;
};
在该lambda中,func是一个可以绑定到任何可调用对象(无论左值右值)的universal reference, args是一个universal reference参数包,它可以绑定到任意类型、任意数量的对象,由于universal reference的存在,timeFuncInvocation几乎可以完成任何函数调用。(Item30说明了为什么是几乎任何)。
总结
- 如果参数为“Type &&”形式且触发了类型推衍,又或者存在“auto &&”,则当前为universal reference。
- 如果不满足上一条,则为右值引用。
- universal refernce被左值初始化时表现为左值,被右值初始化时表现为右值。