来源:https://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers
参考:http://bajamircea.github.io/coding/cpp/2016/04/07/move-forward.html

c++11最重要的特性也许就是右值引用了;move语义跟完美转发都是基于右值引用建立起来的。

右值引用的声明形式跟左值引用的声明一样,只是多了一个‘&’。但是并非有‘&&’出现就表明这是一个右值引用。下面有四个例子:

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

“&&”出现在类型声明时,可能有下面两种意思:

  • 能确定下来,就是右值引用
  • 不能确定下来,可以是右值引用或左值引用,要看实际上下文来定。这种情况,这种引用,有个专门的词,叫forwarding reference(转发引用)

区分上面的两种情况还是很有挑战性的,我们先一句话说明一下:

  • 如果一个变量或参数的声明有T&&的形式,并且T是需要推导的类型(deduced type T),那么这个变量或参数是个forwarding reference.(划重点,deduced type T)

哪里有type deduction,哪里才有forwarding reference.一般下面几种情况可能会出现forwarding reference:

  • parameters to function templates
  • auto-declared variables
  • typedef
  • decltype

forwarding reference必然有T&&的形式。

像所有其他引用一样,forwarding reference必须初始化。初始化决定了,该引用是左值引用还是右值引用:

  • 被一个左值初始化,那么就是左值引用
  • 被一个右值初始化,那么就是右值引用

如何区分左值还是右值?
如何可以拿到一个expression的地址,那么这个expression是个左值。如果expression是个左值引用 (e.g., T& or const T&, etc.), 那么这个expression是个左值。
其他情况都是右值。右值指的就是临时对象(temporary objects),比如函数返回值,通过隐式类型转换(implicit type conversions)生成的对象。 大部分字面值(literal values )(e.g., 10 and 5.3) 都是右值。
再次考虑下文章开头的代码:

Widget&& var1 = someWidget;
auto&& var2 = var1;

可以获取var1的地址,所有var1是个左值。var2有类型推导,有T&&的形式,是个forwarding reference,因为使用var1(一个左值)来初始化的,所以var2 变成了一个左值引用. var2的声明跟下面的等效:

Widget& var2 = var1;

如果一个expression是左值引用,那么它是一个左值:

std::vector<int> v;
...
auto&& val = v[0];               // std::vector<int>::operator[]返回vector中的一个元素的左值引用,val变成了左值引用
template<typename T>
void f(T&& param);               // “&&” might mean rvalue reference,Given this call to f,
f(10);                           // 10 is an rvalue

参数通过10来初始化,10是个右值。forwarding reference参数通过右值初始化,所以参数变成了一个右值引用,确切说是int&&。

如果 f 像下面这样调用

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

参数通过变量x初始化, x是个左值. forwarding reference参数通过左值初始化,所以参数变成了一个左值引用,确切说是int&。

The comment next to the declaration of f should now be clear: whether param’s type is an lvalue reference or an rvalue reference depends on what is passed when f is called. Sometimes param becomes an lvalue reference, and sometimes it becomes an rvalue reference. param really is a universal reference.
参数是左值引用还是右值引用取决于在调用时调用方所传的实参。形参既可能是左值引用也可能是右值引用,形参是个forwarding reference。

有T&&的形式,有类型推导,那么&&表示一个forwarding reference。反过来说,如果没有类型推导,那么就不能说T&&是forwarding reference。

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&& (where T is a template parameter), T需要type deduction, 那么是个forwarding reference. 如果&&在一个具体类型后面,那么是个右值引用。

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

这里有&&,也有类型推导,但是形式不是T&&,而是std::vector&&。所以,param是个普通的右值引用,而不是forwarding reference。
forwarding reference只存在于“T&&”形式中,即使加个const也不行。

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

看个有T&&形式,但是没有类型推导的例子:

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

这里push_back的参数x是个右值引用。我们可以看下这个函数的类外定义形式:

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

push_back 属于 std::vector that contains it. 如果有了 std::vector, 那么就已经知道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的push_back函数。实例化时的声明类似下面这样:

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

看到没?类的实例化完成时,函数的参数类型已经确定了,不需要再推导了。

std::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的参数不受T影响,即使T确定下来,Args依然需要推导,类外声明:

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

所以emplace_back的参数是forwarding reference,push_back的参数是右值引用。

最后一点:一个expression是左值还是右值,跟这个expression的类型没关系。拿int来举例:声明为整形的变量,是个左值;字面值10,是右值;但他们都是整形。也就是说,左值还是右值这个概念跟类型没关系。所以,

  • 一个左值,它的类型可以是右值引用
  • 一个右值,它的类型可以时右值引用
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可以方便的把一个左值转化为一个右值:

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

使用static_cast主要是为了显示,这个expression是个右值引用 (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 w1;
...
Widget& & w2 = w1;               // error! No such thing as “reference to reference”

forwarding reference的模版参数的类型推导中,同样类型的左值跟右值,推导出来的类型是不一样的。

  • 左值推导为T&
  • 右值推导为T
    考虑下,如果一个forwarding reference的模版参数,实例化时,分别用了左值跟右值,会有什么区别?
template<typename T>
void f(T&& param); 
... 
int x; 
... 
f(10);                           // invoke f on rvalue
f(x);                            // invoke f on lvalue

如果调用时用了右值10,那么推导出来的T是int,实例化后如下所述:

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

如果调用时用了左值x,那么推导出来的T是int&,实例化后如下所述:

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

出现了引用的引用,按理说这样的代码是无效的,但是“f(x)”是完全合理的。c++11用了引用折叠(reference collapsing)来解决这个问题。
引用类型有两种,那么引用的引用类型就有四种:

  • & &
  • & &&
  • && &
  • && &&
    右值引用的右值引用会变成右值引用,其他情况都会变成左值引用。
    根据这个规则,上面的代码会变为:
void f(int& param);              // instantiation of f with lvalue after reference collapsing

forwarding reference在进行类型推导前,会先对参数进行reference-stripping,也就是说去掉参数的引用部分,只留type,例子如下:

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

f(r1); 
f(r2);

r1跟r2的类型都是int&。r1,r2在类型推导前会剥离掉引用,只剩int类型,然后它们都是左值,那么就是int&了。

引用折叠除了在模版参数实例化有用到,在auto变量的定义中也有用到。他们的类型推导规则一样:

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时,引用部分被忽略了,只被当作 Widget类型. 因为是左值,推导出来的type是 Widget&. auto替换为 Widget& :

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

which, after reference collapsing, becomes

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

第三个有引用折叠出现的场景是 typedef

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

如下的代码中:

Widget<int&> w;

Widget包含下面的定义:

typedef int& & LvalueRefType;

引用折叠为

typedef int& LvalueRefType;

最后一个出现引用折叠的场景是:decltype.
跟auto一样,decltype推导出的类型有两种:T 或 T&,类型定了后,在用引用折叠规则。
跟auto不同的是,有名的非引用类型,会被推到为 T,同样的情况下,auto会推导为 T&

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.

总结:
forwarding reference 依赖两个规则:

  1. Reference collapsing(& always win)
  2. special type deduction for T&&(左值推导为T&,右值推导为T)
posted on 2020-03-13 11:38  ConfuciusPei  阅读(1423)  评论(0编辑  收藏  举报