Universal References in C++11 — Scott Meyers

或许C++11中最重要的新特征是右值引证;右值引证是移动语义和完美转发的根底(关于右值引证,移动语义,完美转发能够参考Thomas Becker’s overview)。

在语法上,右值引证和规范引证相似,区别是运用两个&,这个函数运用了Widget的右值引证:

void f(Widget&& param);

考虑到右值引证是运用“&&”声明的,似乎能够合理地假定类型声明中呈现“&&”表示一个右值引证,现实并非如此:

Widget&& var1 = someWidget;     // here, “&&” means rvalue reference
auto&& var2 = var1;             // here, “&&” does not mean rvalue reference
template<typename T>
void f(std::vector<T>&& param); // here, “&&” means rvalue reference
template<typename T>
void f(T&& param); // here, “&&”does _not_ mean rvalue reference

在本文中,我描绘了“&&”在类型声明中的两种意义,解说如何区别它们,并引证了新的术语以便能简练的解说&&。区别不同的意义是十分重要的,由于假如你把一切类型声明中的“&&”当成“右值引证”,你会误读很多的C++11代码。

本质上,在类型声明中,&&有时意味着右值引证,有时意味着 既是右值引证又是左值引证。一切一些源码中的 &&实际上有“&”的意义。引证(Universal References)或许比左值引证或右值引证更灵活。例如,右值引证仅能绑定到右值,左值引证能够绑定到左值,且在某些严格状况下能够绑定到右值。“&&”声明的引证能够是左值引证或右值引证,能够绑定到任何东西。这种异常灵活的引证值得具有自己的名字。我称它们为通用引证(Universal References)。

&&何时表示为通用引证的细节是复杂的,所以我把解说放到后边。现在,让咱们重视以下经验法则,这些在日常编程中需要记住的:

  • 假如一个变量或参数的声明是 T&& 类型(其间T是被揣度的类型),那么这个变量或参数是一个通用引证。

类型揣度的要求限制了通用引证被发现。实际上几乎一切的通用引证是函数模版的参数。由于关于auto声明的变量的类型揣度的规矩是和模版相同的,所以有auto声明的也或许是一个通用引证。这些在出产代码中是不常见的,可是我将在文章中展现这些比如,由于它们比模版更简练。在Nitty Gritty Details section 部分,我解说了在运用typedef和decltype时也或许呈现通用引证,可是在咱们深化了解基本细节之前,我将持续评论通用引证,就好像通用引证只与函数模板参数和auto声明的变量有关一样。

通用引证的形式是T&&的约束比它自身看起来更重要,但我将把它的查看推迟到稍后。现在,请简略地记住这个需求。

和一切引证一样,通用引证有必要被初始化,而且通用引证的初始化决定了它是一个左值引证或右值引证:

  • 假如用一个左值初始化一个通用引证,通用引证变成一个左值引证。
  • 假如用一个右值初始化一个通用引证,通用引证变成一个右值引证。

这个信息关于区别左值和右值是有用的。这些术语很难精确界说(C++ 11规范一般根据具体状况指定表达式是左值仍是右值),可是在实践中,以下内容是满足的:

  • 假如你能对一个表达式取址,这个表达式是一个左值
  • 假如一个表达式的类型是一个左值引证(T& 或 const T&),这个表达式是一个左值
  • 否则,表达式是一个右值。概念上,右值和临时目标有关,例如函数回来的目标或通过隐式类型创立的目标。大多数字面量也是右值。

再次考虑文章开端时的代码:

Widget&& var1 = someWidget;     // here, “&&” means rvalue reference
auto&& var2 = var1;             // here, “&&” does not mean rvalue reference

var1 能够取址,所以var1 是一个左值,var2 的auto&&的类型声明使它成为一个通用引证,而且由于var2var1 (左值)初始化,所以var2 变成了一个右值引证。假如没有仔细阅读源码,会让你把var2当成一个右值引证;&&的呈现暗示这个定论。可是var2是有左值初始化的通用引证,所以var2成为了左值引证。就像var2是这样声明的:

Widget& var2 = var1;

如上所述,假如表达式具有左值引证类型,则它是左值,考虑这个比如:

std::vector<int> v;
...
auto&& val = v[0];    // val becomes an lvalue reference (see below)

val 是通用引证,由std::vector<int>::operator[] 调用的结果v[0] 进行初始化。这个函数回来vector中元素的左值引证。一切的左值引证是左值,而且val用这个左值进行了初始化,val变成了左值引证,即使声明看起来像右值引证。

通用引证常常运用作为函数模版的参数,考虑文章开端的模版:

template<typename T>
void f(T&& param);    // “&&”does not mean rvalue reference

对f调用,

f(10);     // 10是右值

参数被字面量10初始化,由于10不能被取址,所所以右值。这意味着在f调用中,通用引证参数被一个右值初始化,所以这个参数变成了一个右值引证,即int &&。

另一方面,假如f像这样被调用:

int x = 10;
f(x);        // x is an lvalue

参数被x初始化,由于x能够被取址,所所以左值。这意味着在f调用中,通用引证参数被一个左值初始化,所以这个参数变成了一个左值引证,即int &。

函数f的注释应该是很明晰的:是否参数的类型是一个左值或右值引证依赖于f被调用时传递的内容。param 是一个通用引证。

仅当类型揣度产生时,“&&”是一个通用引证。没有类型揣度的状况下,是不存在通用引证的。“&&”在类型声明中总是代表右值引证。因而:

template<typename T>
void f(T&& param);               // deduced parameter type ⇒ type deduction;
                                 // && ≡ universal reference
template<typename T>
class Widget {
    ...
    Widget(Widget&& rhs);        // fully specified parameter type ⇒ no type deduction;
    ...                          // && ≡ rvalue reference
};
template<typename T1>
class Gadget {
    ...
    template<typename T2>
    Gadget(T2&& rhs);            // deduced parameter type ⇒ type deduction;
    ...                          // && ≡ universal reference
};
void f(Widget&& param);          // fully specified parameter type ⇒ no type deduction;
                                 // && ≡ rvalue reference

在每个比如中,假如你看T&&(T是一个模版参数),具备类型揣度,所以你正在看一个通用引证。假如你在一个具体类型(例如Widget&&)后边看到&&,你正在看一个右值引证。

引证声明的格局有必要是“T&&”才会使引证变成通用引证。这是十分重要的。咱们来看文章开端的声明:

template<typename T>
void f(std::vector<T>&& param);     // “&&” means rvalue reference

这段代码中存在类型揣度和“&&”声明的函数参数,可是参数声明的格局不是“T&&”,而是“std::vector<T>&&”。所以这个参数是一个规范的右值引证,不是一个通用引证。通用引证仅能呈现在“T&&”格局!甚至一个简略的const修饰符也是能够禁用“&&”解说为通用引证:

template<typename T>
void f(const T&& param);  // “&&” means rvalue reference

有时你能够在函数模版声明中看到T&&,可是这仍不是类型揣度。考虑vector中的push_back函数:

template <class T, class Allocator = allocator<T> >
class vector {
public:
    ...
    void push_back(T&& x);       // fully specified parameter type ⇒ no type deduction;
    ...                          // && ≡ rvalue reference
};

这里的T是一个模版参数,而且push_back函数运用了T&&,然而这个参数不是通用引证!那怎样样才是呢?

假如咱们看到push_back是怎样在类外声明的,答案是十分显着的。为了使代码更明晰,假定vector的Allocator参数不存在,由于它和评论无关。push_back的声明如下:

template <class T>
void vector<T>::push_back(T&& x);

假如没有包括它的std::vector<T>类,push_back就不存在。可是假如咱们有一个类std::vector<T>,咱们现已知道T是什么,所以没有必要推导它。

这个比如将会协助你了解:

Widget makeWidget();             // factory function for Widget
std::vector<Widget> vw;
...
Widget w;
vw.push_back(makeWidget());      // create Widget from factory, add it to vw

push_back的运用将使编译器实力化类std::vector<Widget>中的函数。push_back的声明是如下这样:

void std::vector<Widget>::push_back(Widget&& x);

看,只需咱们知道了std::vector<Widget>,push_back的参数类型现已被完好的揣度出来了。所以这里是不存在类型揣度操作的。

作为对此,vector中的emplace_back函数声明如下:

template <class T, class Allocator = allocator<T> >
class vector {
public:
    ...
    template <class... Args>
    void emplace_back(Args&&... args); // deduced parameter types ⇒ type deduction;
    ...                                // && ≡ universal references
};

不要由于emplace back接受了数量可变的参数(如Args和args声明中的省略号所示)而疏忽了有必要推导每个参数的类型这一现实。函数模版参数Args是独立于类模版参数T的,所以即使咱们知道类是std::vector<Widget>,不能得出emplace_back的函数参数的类型。std::vector<Widget>的emplace_back的类外声明如下(相同疏忽Allocator):

template<class... Args>
void std::vector<Widget>::emplace_back(Args&&... args);

明显,知道类是std::vector<Widget>并不能消除编译器揣度参数类型的需要。所以emplace_back的参数是通用引证。

最终值得记住的一点是:表达式是左值仍是右值是和表达式的类型无关的。关于类型int,int s 是int类型的左值,字面量10是int类型的右值。这是和自界说类型Widget一样的。Widget目标能够是左值(例如,Widget变量)或右值(例如,从Widget创立工厂函数回来的目标)。一个表达式的类型并不能让你知道它是一个左值仍是右值。由于表达式是左值仍是右值是和表达式的类型无关的,所以有右值引证类型的左值(var1),也有右值引证类型的右值(static_cast<Widget&&>(var1))。

Widget makeWidget();                       // factory function for Widget
Widget&& var1 = makeWidget()               // var1 is an lvalue, but
                                           // its type is rvalue reference (to Widget)
Widget var2 = static_cast<Widget&&>(var1); // the cast expression yields an rvalue, but
                                           // its type is rvalue reference  (to Widget)

从左值转换到右值的传统方式是运用std::move,所以var2能够这样界说:

Widget var2 = std::move(var1);             // equivalent to above

我运用static_cast仅是为了使表达式的类型是一个右值引证(Widget&&)这个概念更明晰。

右值引证类型的命名变量和参数是左值(能够对它们取地址)。考虑如下代码:

template<typename T>
class Widget {
    ...
    Widget(Widget&& rhs);        // rhs’s type is rvalue reference,
    ...                          // but rhs itself is an lvalue
};
template<typename T1>
class Gadget {
    ...
    template <typename T2>
    Gadget(T2&& rhs);            // rhs is a universal reference whose type will
    ...                          // eventually become an rvalue reference or
};                               // an lvalue reference, but rhs itself is an lvalue

在Widget的结构函数中,rhs是一个右值引证,所以咱们知道它被绑定到一个右值(也便是说,一个右值被传递给它),可是rhs自身是一个左值,所以假如咱们想要运用它所绑定目标的右值,咱们有必要将它转换回一个右值。咱们这样做的动机一般是将其用作移动操作的源,这便是为什么将左值转换为右值的方法是运用std::move。相似地,Gadget的结构函数中的rhs是一个通用引证,因而它能够绑定到左值或右值,但不管它绑定到什么,rhs自身都是一个左值。假如它被绑定到一个右值而咱们想要运用它被绑定目标的右值,咱们有必要把rhs转换回一个右值。假如它被绑定到一个左值,当然,咱们不想把它当作一个右值。关于通用引证所绑定目标的左值和右值的这种模糊性,运用std::forward接受通用引证左值,并仅在它所绑定的表达式为右值时将其转换为右值。函数的称号(“forward”)标明,函数的称号(“forward”)标明,咱们希望在传递(转发)给另一个函数时保留调用参数的左值性或右值性。

std::movestd::forward不是这篇文章的要点,实际上“&&”能够声明右值引证,也能够不是右值引证。为了防止丢失要点,能够在参考资料中了解move和forward。


通用引证(universal reference)的细节

问题的真正中心在于c++ 11中的某些结构会产生对引证的引证,而对引证的引证在c++中是不允许的。假如源代码显式地包括对引证的引证,则代码无效:

Widget w1;
Widget& & w2 = w1;

可是,在某些状况下,对引证的引证是由于编译期间产生的类型操作而产生的,在这种状况下,拒绝代码或许会有问题。咱们从c++的初始规范(即c++ 98/ c++ 03)的经验中知道这一点。

在为通用引证的模板形参进行类型推导期间,将同一类型的左值和右值推导为具有稍微不同的类型。特别是类型T的左值被揣度为T&,T的右值被简略地揣度为T(左值被揣度为左值引证,右值没有被揣度为右值引证!)。考虑一个带有通用引证参数的函数模版被右值和左值调用时产生了什么:

template<typename T>
void f(T&& param);
...
int x;
...
f(10);                           // invoke f on rvalue
f(x);                            // invoke f on lvalue

在运用右值10调用f时,T被揣度为int,f的实例化时:

void f(int&& param);             // f instantiated from rvalue

然而在运用左值x调用f时,T被揣度为int&,f的实例化包括了一个引证的引证:

1.  void f(int& && param); // initial instantiation of f with lvalue

由于reference-to-reference的原因,实例化代码是无效的,可是源码“f(x)”是完全有理由的这么用的。为了防止拒绝这样的代码,当存在引证的引证时,C++11履行“引证折叠”(reference collapsing)。

由于存在两种引证,所以一共有四种组合方式:

  • 左值引证到左值引证 & &
  • 左值引证到右值引证 & &&
  • 右值引证到左值引证 && &
  • 右值引证到右值引证 && &&

一共有两种折叠规矩:

  • 右值引证到右值引证成为(折叠)右值引证
  • 其他一切的成为左值引证

根据规矩,关于一个左值,f的实例化变成有用的代码,编译器会这样处理此次调用:

void f(int& param);  // instantiation of f with lvalue after reference collapsing

这标明了通用引证在通过类型揣度和引证折叠之后或许成为左值引证的机制。现实是,通用引证只是引证折叠上下文中的右值引证。

当揣度一个自身便是引证的变量的类型时,工作变得愈加微妙。在这种状况下,类型的引证部分将被疏忽。例如:

int x;
...
int&& r1 = 10;                   // r1’s type is int&&
int& r2 = x;                     // r2’s type is int&

在调用模板f时,r1和r2的类型都被认为是int。这种引证剥离行为与以下规矩无关:在通用引证的类型推导过程中,左值被推导为T&类型,右值被推导为T类型。

f(r1);
f(r2);

r1和r2都被揣度为int&。为什么?首先r1和r2类型的引证部分被去除(变成int),然后由于都是左值,所以在f的调用中通用引证的类型揣度都是int&。

正如我所指出的,引证折叠产生在“模板实例化的上下文中”。第二种相同的状况是auto变量的界说。关于通用引证的auto变量的揣度与函数模版中的通用引证参数的揣度是相同的,类型T的左值被揣度为T&,类型T的右值被揣度为T。考虑文章开端时的比如:

Widget&& var1 = someWidget;      // var1 is of type Widget&& (no use of auto here)
auto&& var2 = var1;              // var2 is of type Widget& (see below)

var1的类型是Widget&&,可是在初始化var2的类型揣度过程中,var1的引证性是被疏忽的,被当作类型Widget。由于它是一个左值,被用来初始化通用引证(var2),它的揣度类型是Widget&。在var2的界说中用Widget&替换auto将产生以下无效代码:

Widget& && var2 = var1;          // note reference-to-reference

在引证折叠后,变成:

Widget& var2 = var1;             // var2 is of type Widget&

第三种会产生引证折叠的上下文是typedef的运用。给予一个类模版:

template<typename T>
class Widget {
    typedef T& LvalueRefType;
    ...
};

和这个模版的运用:

Widget<int&> w;

实例化之后的类将包括以下代码(无效):

typedef int& & LvalueRefType;

引证折叠将其简化为以下合法代码:

typedef int& LvalueRefType;

假如咱们带有引证的上下文中运用这个typedef,例如:

void f(Widget<int&>::LvalueRefType&& param);

在typedef替换之后,以下无效代码生成:

void f(int& && param);

可是引证折叠产生了,所以f的终究声明是:

void f(int& param);

最终一种会产生引证折叠的上下文是decltype的运用。与模板和auto的状况一样,decltype对生成类型为T或T&的表达式履行类型推导,然后decltype运用c++ 11的引证折叠规矩。遗憾的是,decltype运用的类型推导规矩与模板或auto类型推导期间运用的规矩不同。细节过于晦涩,无法在这里进行介绍(参考资料提供了进一步的信息),可是一个值得注意的区别是,给定一个非引证类型的命名变量,decltype会揣度出类型T(非引证类型),而在相同的条件下,模板和auto揣度出类型T&。另一个重要的区别是decltype的类型推导只依赖于decltype表达式;初始化表达式的类型(假如有的话)将被疏忽。例如:

Widget w1, w2;
auto&& v1 = w1;          // v1 is an auto-based universal reference being
                         // initialized with an lvalue, so v1 becomes an
                         // lvalue reference referring to w1.
decltype(w1)&& v2 = w2;  // v2 is a decltype-based universal reference, and
                         // decltype(w1) is Widget, so v2 becomes an rvalue reference.
                         // w2 is an lvalue, and it’s not legal to initialize an
                         // rvalue reference with an lvalue, so
                         // this code does not compile.

总结

在类型声明中,“&&”能够是右值引证,也能够是通用引证 —— 一个能够解析为左值引证或右值引证的引证。通用引证总是T&&的格局,其间T是需要被揣度的类型。

引证折叠机制使通用引证有时解析为左值引证,有时解析为右值引证。引证折叠产生在编译期间呈现引证的引证的上下文中。这些上下文能够是模版类型揣度、auto类型揣度,typedef的运用,decltype表达式。


更多参考资料

C++11, Wikipedia.

Overview of the New C++ (C++11), Scott Meyers, Artima Press, last updated January 2012.

C++ Rvalue References Explained, Thomas Becker, last updated September 2011.

decltype, Wikipedia.

“A Note About decltype,”Andrew Koenig, Dr. Dobb’s, 27 July 2011.