24 Distinguish universal references from rvalue references
为了声明 T
的右值类型,写作 T&&
。但是并不是所有的 T&&
都是右值。
void f(Widget &¶m); // rvalue reference
Widget &&var1 = Widget(); // rvalue reference
auto &&var2 = var1; // not rvalue reference
template <typename T>
void f(std::vector<T> &¶m); // rvalue reference
template <typename T>
void f(T &¶m); // not rvalue reference
T&&
有两个语义。一个是右值引用,绑定到右值,表示对象可以被移动。
另一个语义是是左值引用或者是一个右值引用。虽然看起来像是右值引用,但是实际是左值引用。因此,既可以绑定右值,也可以绑定左值。进一步,可以绑定 const
或者是非 const
对象,也可以绑定 volatile
或者是非 volatile
对象。甚至可以绑定到 const volatile
对象上。几乎可以绑定到任意对象上。因此,称为通用引用(universal references
)。
通用引用往往出现在两个上下文中。第一个是模板函数参数。
第二个是auto
声明。
两者的共同点是有类型推导。而下面的两个例子不涉及类型推导,那么就不是通用引用而是右值引用。
void f(Widget &¶m); // 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 &¶m); // param is a universal reference
Widget w;
// lvalue passed to f; param's type is Widget& (i.e., an lvalue reference)
f(w);
// rvalue passed to f; param's type is Widget&& (i.e., an rvalue reference)
f(std::move(w));
T&&
。
除非我们指定模板函数的模板参数,否则 T
需要被推导。但是 param
的类型是 std::vector<T> &&
而不是 T&&
,因此 param
是右值引用。如果绑定左值,编译器会报错
即使简单的加上 const
修饰,也会使其是非通用引用。
当看到一个参数类型是 T&&
的模板函数,也不能假设它就是通用引用。因为不能假定一定会出现类型推导。考虑 std::vector
中的 push_back
函数。
// from C++ Standards
template <class T, class Allocator = allocator<T>>
class vector
{
public:
void push_back(T &&x);
};
push_back
的参数类型是符合要求的,但是这里不涉及类型推导。因为 vector
没有实例化的话,push_back
不存在,而一旦实例化了之后,push_back
的类型就完全确定了。比如
实例化的 std::vector
如下所示
class vector<Widget, allocator<Widget>>
{
public:
void push_back(Widget &&x); // rvalue reference
};
push_back
不涉及类型推导,这里总是右值引用。
与之相对,std::vector
的成员函数 emplace_back
就涉及类型推导。
// still from C++ Standards
template <class T, class Allocator = allocator<T>>
class vector
{
public:
template <class... Args>
void emplace_back(Args &&...args);
};
Args
类型与 std::vector
的参数类型 T
无关,那么每次调用 emplace_back
的时候,都会涉及类型推导。
这里参数名字是 Args
,形式也是 T&&
。当然,形式与 T
这个名字无关,所以精确地说通用引用的形式是 type&&
,且 param
需要类型推导。
template <typename MyTemplateType> // param is a
void someFunc(MyTemplateType &¶m); // universal reference
auto&&
,也是 T&&
形式,也涉及类型推导。假定在 C++14 中,实现一个函数,统计任意函数调用的时间开销。
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(xxx)>
略微有点复杂,Item 33 会解释。这里的重点是 lambda 中的参数类型是 auto&&
。所以 func
是通用引用,可以绑定任意可调用对象,左值或者右值都行。params
是零个或多个通用引用,可以绑定任意多个、任意类型的参数。有了 auto
通用引用 timeFuncInvocation
可以几乎可以测量任意函数的执行时间(Item 30 会说明为什么是几乎任意而不是任意函数)。
通用引用只是一个抽象,实际是引用折叠(reference collapsing
),详见 Item 28。真相引用折叠并不会让通用引用变的无用。区分通用引用和右值引用,帮助我们更好的理解代码,帮助我们更好的和同时沟通。Item 25 和 Item 26 也是基于这一点进行深入分析的,所以区分二者很重要。
Things to Remember
- If a function template parameter has type
T&&
for a deduced typeT
, or if anobject is declared usingauto&&
, the parameter or object is a universal reference. - If the form of the type declaration isn't precisely
type&&
, or if type deduction does not occur,type&&
denotes an rvalue reference. - Universal references correspond to rvalue references if they're initialized with rvalues. They correspond to lvalue references if they're initialized with lvalues.