现代C++的引用操作

先要理解什么是C++的值

cppreference对Value做了解释

C++11之前,lvalue,rvalue可以简单的理解为等号两边的值类型。但C++11后,语言对用户开放了更多的辅助类型以帮助设计者开发更好用的STL库。比较典型的就是引入glvalue(general left value), xvlaue( eXpiring value, for it is temporary value at most of time, it explained in stroustroup’s article), prvalue( pure right value).

lvalue:    有identity/address (这里的identity是指可以用来和其它值用来比较的一个addressable address。不能比较的表达式意味着没有Identity), 但是这个不能被move,转移它所拥有的值或指针。通常情况下见到的变量都是lvalue。

xvalue; 既有identity,又可以被move。比如函数的返回值。

prvalue: 没有identity,但可以被move。比如某个常量值。数字1,2,常量字符串 “rock”.

StackOverFlow有一个不错的图来解释左值和右值。

Taxonomy
C++11引入的move语改变传统lvalue与rvalue概念

C++标准进一步解释了xvalue

判断一个表达式是xvalue的依据:

  • 无论是显式还是隐式的函数调用,其返回值是对象类型的右值引用(rvalue reference to object type)
  • 将右值引用强制转换(cast)为一个 对象类型。
  • 非静态非引用类型数据成员访问表达式。如 func().m; a class member access expression designating a non-static data member of non-reference type in which the object expression is an xvalue)
  • 指向成员的指针表达式,如*pbm。pb是xvalue。 (a .* pointer-to-member expression in which the first operand is an xvalue and the second operand is a pointer to data member.)

统一来讲,带名字的右值引用被当做左值,没名字的右值引用被当做xvalue;而如果引用的对象是函数的话,则不管有无名字都是左值。(In general, the effect of this rule is that named rvalue references are treated as lvalues and unnamed rvalue references to objects are treated as xvalues; rvalue references to functions are treated as lvalues whether namedornot)

And as a follow up few examples are given. Here’s an extension to them:

struct A { int m; };

A&& operator+(A, A);
A a;
A b;
a + b;   // xvalue

A&& f();
f();     // xvalue
f().m;   // xvalue

A a;
static_cast<A&&>(a);  // xvalue
std::move(a);         // xvalue

It’s a good “Move”

move本身不对其中的值进行操作,它是配合STL模板函数来完成将A容器的引用指针数据直接转交给B容器。如:

std::vector A(2,3);
std::vector B(std::move(A));

转移发生在B的右值拷贝构造函数中: std::vector(std::vector&& a){ this.data_pointer = a.data_pointer; a.reset();}

xvalue的存在就像薛定谔的猫,在某些场景下,xvalue既可以左又可以右。(这里会列出更多场景)

forward仅仅是为了在参数传递过程中,保持&&类型或&类型而引入。毕竟我们要区分到底是一个 1,还是一个int i=1。

B.S对lvaue,rvalue的解释,我认为对一般人来说不好理解。特别在解释move的解释上偏晦涩。 在编程实现上,std::move/std::forward 这对兄弟可以用来理解URef,RRef,LRef之间的关系。

// move实现 
template<typename _Tp>
    constexpr typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t) noexcept
    { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }


// forward实现一个rvalue的类型传递
 template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    {
      static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
		    " substituting _Tp is an lvalue reference type");
      return static_cast<_Tp&&>(__t);
    }

// forward实现一个lvalue的类型传递
  template<typename _Tp>
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    { return static_cast<_Tp&&>(__t); }



类型推导

参数在推导传递过程中,允许退化(decay, reference collapse)。退化的场景有如下:

T&& -> T&
T&& & -> T&
T& && -> T&
T&& && -> T&&

因此当Foo(&)传入一个&&类型时,我们为了保持右值特性,使用forward保持。在Foo(&&)传入一个&类型时,退化为左值类型T&,也使用forward保持。

Scott Meryer 对这个问题展开了更有意思的解释

他引入一个新的概念来解释这个令人费解的特性:URef (universal reference),把URef比作随着温度而决定性别的火鸡。

  • T&& , auto&&, typedef T&&, decltype(T&&) T是一个符号,帮助类型推导。我们称它“通用引用”。 它既可以Bind一个左值(lvalue)后成为一个左值引用(LRef),也可以Bind一个右值(rvalue)成为一个右值引用(RRef)。[注意是 T]
  • 那你说, vector::push_back(T&&)是一个URef吗? 不,vector的这个函数已在编译时实例化时确定了T,如vector::push_back(Widget&&), 所以是rvalue引用。

来看几个例子:

/*auto deduction example*/
vector<int> aVector(6);    //aVectorp[5] 是一个典型的lvalue
auto&& aRef = aVector[5];  //推导为vector<int>&, URef绑定后成为LRef。
auto&  bRef = aVector[5];  //推导为vector<int>&, LRef绑定LRef。
auto&& cRef = std::move(aVector); //推导为vector<int>&&, URef绑定rvalue成为RRef

int&&  rri = 5;          // 无推导,类型为int&&
auto   autoRR = rri;     // 推导为int, 即使rri是const int&& ,去掉const和&&
auto&  autoRRR = rri;    // 推导为int&
auto&& autoRRRR = rri;   // 推导为int&
auto&& autoRRRRRR = autoRRR; //推导为int&
auto   autoX = autoRRRRRR;   //推导为int,即使autoRRRRRR是const int&&。
auto&& autoY = rri;      // 按照collapse规则,应该&&,但是这里推导为&
                         // 按照Scott的解释,rri初始化URef已经成为一个LRef
/*decltype example*/
Widget w;
decltype(w)&& r1 = std::move(w); // r1 type is Widget&&
decltype((w))&& r2 = std::move(w); // (w) is a expression,return &, r2 is Widget&

总结起来为以下:

  • type&& 并不等价于 右值引用(rvalue reference)
  • type&& 用于 语法 + 类型推导,我们称之为通用引用(URef, Universal Reference.)
  • URef最后是LRef还是RRef取决于初始化。LValue=>LRef, RValue =>RRef.
  • 并不是所有模板的T&&是URef。比如Vector::push_back的&&参数版本。
  • 重载URef通常是不对的。行为不是你所预期。
  • 类型推导和引用退化(reference collapsing)是需要URef的基本场景:模板,auto,typedef, decltype。

现代C++的引用操作》上有1个想法

  1. ChangeLog:

    对value的类型进一步细分。
    讲完概念后,分场景讲Reference。
    每个场景给出对应的例子帮助理解。
    回到概念,进一步理解概念。

发表评论

电子邮件地址不会被公开。 必填项已用*标注