经常说,真相会使人自由,但在某些环境下,一个精心挑选的谎言也同样使人释放。本条款就是这样一个谎言。但因为我们面对的是软件,我们还是不谈“谎言”,我们说本条款包含了一个“抽象”。
为了声明一个指向某类型T的右值引用,你会写成T&&。于是很合理的设想,你在代码中看到“T&&”,你会把它当作一个右值引用。但其实并没有这么简单:
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&&),但它们可以表现的像是左值引用。它们的双重特性使之可以既绑定到右值(像右值引用一样),也可以绑定到左值(像左值引用)。此外它们也可以绑定到const或非const对象,以及volatile或非volatile对象,甚至绑定到那些既是const也是volatile的对象。它们可以虚拟的绑定到任何东西上。这史无前例的灵活性需要一个它们自己的名字,我管它们称为统一引用。
统一引用出现在两种场景下。最常见的是函数模板的参数,比如下面这个例子(来自上面的示例代码):
template<typename T>
void f(T&& param); // param 是一个统一引用
第二个场景是auto声明,统一来自上面示例代码
auto&& var2 = var1; // var2 是一个统一引用
这些场景共同之处是有类型推导的出现。在模板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
因为统一引用是引用,所以必须初始化。统一引用的初始化者决定了它代表左值还是右值引用。初始化者如果是右值,则统一引用关联到一个右值引用。如果初始化者是个左值,则统一引用关联到一个左值引用。对于函数参数的统一引用,初始化者如下提供:
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&&”。再看看早前的示例代码:
template<typename T>
void f(std::vector<T>&& param); // 参数是右值引用
当f被调用,T类型会进行推导(除非调用者显式的指明它,边界情况我们不关注),但param的类型声明不是“T&&”,是“std::vector<T>&&”。这排除了param是一个统一引用的可能性,因此param是个右值引用。假如你打算传递一个左值给f,编译器会很乐意给你报错:
std::vector<int> v;
f(v); // error! can't bind lvalue to
// rvalue reference
即使是最简单的const符也足够使得引用不再是一个统一引用:
template<typname T>
void f(const T&& param) //param is an rvalue reference
假如在一个模板中,你看到一个函数参数是类型T&&,你可能会认为它一定是统一引用。不一定。因为在模板中不能断定一定有类型推导。考虑下面这个在std::vector中的push_back成员函数:
template<class T, class Allocator = allocator<T>> // from C++
class vector { // Standards
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类型的右值引用。
相反,vector里概念上相似的成员函数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的,所以,Args必须在每次emplace_back被调用时进行推导(当然,Args实际上是个参数包(pack),不是真正的类型参数,但出于这里讨论的目的,我们可以假设它是个类型参数)。
emplace_back的类型参数是Args,仍然是一个统一引用,这个事实强化了我之前说的统一引用的形式是“T&&”,但没必要一定要用T。比如下面的模板用了统一引用,因为形式(type&&)是对的,param的类型会被推导(除了那些调用者明确指定了类型的):
template<typename MyTemplateType> // param is a
void someFunc(MyTemplateType&& param); // universal reference
我之前也说过auto变量也可以作为统一引用。更准确的说是声明类型为auto&&的变量是统一引用,因为发生了类型推导并且它们有正确的形式(“T&&”)。auto统一引用用在函数模板参数中没有统一引用多,但它们的确时不时的出现在c++11中。在C++14中,它们出现的更多,因为c++14的lamda表达式可以声明auto&&参数。比如,你想写一个c++14的lamda表达式来记录任意函数调用中的时间消耗,你可以这样:
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)>”代码很困惑,很可能是你没有看过条款33,没关系,在本条款里,这里的重点是这个lamda声明的auto&&参数。func是一个统一引用,可以被绑定到任何可调用对象,左值或右值。params是0个或更多的统一引用(也就是一个统一引用参数包),可以被绑定任何数量的任意类型的对象。多亏了auto统一引用,结果是timeFuncInvocation可以计算出大多数任何函数执行时间。(关于“任何”和“大多数任何”的区别,可以见条款30).
整个本条款需要记住的是,统一引用的概念是个谎...哦是个“抽象”。隐藏的事实是引用倒塌,条款28会描述。但事实并没有使得这个抽象无用。区分开右值引用和统一引用会帮助你更精确的阅读代码(“我看到的T&&是只绑定了右值还是任何东西?”),它也可以避免你和同事交流中的似是而非(“我这里用的是统一引用,而不是右值引用...”)。也会使你对条款25和26明白,它们依赖了这个区别。所以拥抱这个抽象吧,在其中狂欢吧。就像牛顿定律(科学上不正确的)比爱因斯坦的相对论(“真理”)更典型的有用,更容易被接受一样,统一引用也比引用倒塌的细节更容易使用。
应该记住的事情
1.假如一个函数模板的参数有一个T&&类型用来推导,或者对象使用auto&&来声明,这个参数或对象是一个统一引用。
2.假如类型声明的形式不是精确的type&&,或者没有发生类型推导,type&&表示了一个右值引用。
3.假如统一引用被右值来初始化,则它们关联到右值。如果被左值来初始化,则它们关联到左值。