C++ 新特性 笔记 2 右值引用

C ++ Rvalue引用说明

如下内容,主要是上述连接的摘要html

介绍

Rvalue引用是C ++的一个特性,它是随C ++ 11标准添加的。使右值参考有点难以理解的是,当你第一次看到它们时,不清楚它们的目的是什么或它们解决了什么问题。所以,我不会直接进入并解释rvalue引用是什么。相反,我将从要解决的问题开始,而后展现右值引用如何提供解决方案。这样,右值参考的定义对您来讲彷佛是合理和天然的。
Rvalue引用解决了至少两个问题:c++

&embp * 实现移动语义
&embp * 完美转发算法

从C的最先期开始的左值和右值的原始定义以下:左值是能够出如今赋值的左侧或右侧的表达式,而右值是只能出如今赋值表达式的右侧。
在C ++中,这仍然是左值和右值的第一个直观方法。可是,C ++及其用户定义类型引入了一些关于可修改性和可赋值性的细微之处,致使该定义不正确。咱们没有必要进一步研究这个问题。这是一个替代定义,虽然它仍然能够与之争论,但它将使你可以处理rvalue引用:lvalue是一个表达式,它引用一个内存位置并容许咱们经过如下方式获取该内存位置的地址:& operator 。右值表达式不是左值。ide

// lvalues:
  //
  int i = 42;
  i = 43; // ok, i is an lvalue
  int* p = &i; // ok, i is an lvalue
  int& foo();
  foo() = 42; // ok, foo() is an lvalue
  int* p1 = &foo(); // ok, foo() is an lvalue

  // rvalues:
  //
  int foobar();
  int j = 0;
  j = foobar(); // ok, foobar() is an rvalue
  int* p2 = &foobar(); // error, cannot take the address of an rvalue
  j = 42; // ok, 42 is an rvalue

移动语义

假设X是一个包含某个资源的指针或句柄m_pResource的类。逻辑上,X的复制赋值运算符 以下所示:函数

X& X::operator=(X const & rhs)
{
  // [...]
  // Make a clone of what rhs.m_pResource refers to.  clone 指向的资源,
  // Destruct the resource that m_pResource refers to.  销毁指向的资源
  // Attach the clone to m_pResource. 将clone 来的资源指向m_pResource
  // [...]
}

相似的推理适用于复制构造函数。如今假设X使用以下:性能

X foo();
X x;
// perhaps use x in various ways
x = foo();

x = foo() 执行了 1) clone foo返回的临时资源, 2) 销毁x自身的资源并用clone的资源替换 3) 销毁临时资源。
显然,交换x和临时对象的资源指针(句柄)会更好,也更有效率,而后让临时的析构函数破坏x原始资源。换句话说,在 = 的右侧是右值的特殊状况下,咱们但愿复制赋值运算符的行为以下:优化

// [...] 
//交换m_pResource和rhs.m_pResource 
// [...]

这称为移动语义。使用C ++ 11,能够经过重载实现此条件行为:this

X& X::operator=(<mystery type> rhs)
{
  // [...]
  // swap this->m_pResource and rhs.m_pResource
  // [...]  
}

右值引用

若是X是任何类型的,则X&&称为右值引用到X。为了更好地区分,普通引用X&如今也称为左值引用。
右值引用是一种与普通引用很是类似的类型,但X&有一些例外。最重要的一点是,当涉及函数重载解析时,左值更倾向旧式左值引用,而右值更倾向于新的右值引用:编码

void foo(X&x); //左值引用 overload
void foo(X && x); // 右值引用 overload 

X x; 
X foobar(); 

FOO(x); //参数是左值:调用foo(X&)
foo( foobar() ); //参数是rvalue:调用foo(X&&)

重点是:.net

rhd 引用容许函数在编译时branch (经过重载解析), 条件是 "我是在 lvalue 上被调用仍是在 rvalue 上被调用?"
确实,能够以这种方式重载任何函数,如上所示。可是在绝大多数状况下,为了实现移动语义,只有复制构造函数和赋值运算符才会出现这种重载:

X& X::operator=(X const & rhs); // classical implementation
X& X::operator=(X&& rhs)
{
  // Move semantics: exchange content between this and rhs
  return *this;
}

为复制构造函数实现rvalue引用重载是相似的。

警告: 正如在C ++中常常发生的那样,乍一看看起来恰到好处仍然有点完美。事实证实,在某些状况下,上面的复制赋值运算符之间this和之间的简单内容交换rhs还不够好。咱们将在下面的第4节“强制移动语义”中再次讨论这个问题。

强制移动语义

C ++ 11容许您不只在rvalues上使用移动语义,并且还能够自行决定使用左值。一个很好的例子是std库函数swap。和之前同样,让X成为一个类,咱们已经重载了复制构造函数和复制赋值运算符,以实现对rvalues的移动语义。

template<class T>
void swap(T& a, T& b) 
{ 
  T tmp(a);
  a = b; 
  b = tmp; 
} 

X a, b;
swap(a, b);

这里没有rvalues。所以 swap函数的三行并无使用 移动语义。但咱们知道移动语义是能够的: 不管变量出如今何处, 该变量都是做为副本构造或赋值的源出现的, 该变量要么根本不被使用, 要么仅用做赋值的目标。在C ++ 11中,有一个std库函数被称为std::move,来拯救咱们。它是一个函数,能够将其参数转换为右值,而无需执行任何其余操做。所以,在C ++ 11中,std库函数swap 以下所示:

template<class T> 
void swap(T& a, T& b) 
{ 
  T tmp(std::move(a));
  a = std::move(b); 
  b = std::move(tmp);
} 

X a, b;
swap(a, b);

如今全部swap函数的三行都会移动语义。请注意,对于那些没有实现移动语义的类型(也就是说,不要使用rvalue引用版本重载它们的复制构造函数和赋值运算符),新swap行为就像旧的行为同样。
如上所述,std::move咱们能够在任何地方 使用swap,为咱们带来如下重要好处:

  • 对于那些实现移动语义的类型,许多标准算法和操做将使用移动语义,所以可能会得到潜在的显着性能提高。一个重要的例子是就地排序:就地排序算法除了交换元素以外几乎没有其余任何东西,这种交换如今将利用全部提供它的类型的移动语义。
  • STL一般须要某些类型的可复制性,例如,可用做容器元素的类型。仔细检查后发现,在许多状况下,可移动性就足够了。所以,咱们如今可使用可移动但不可复制的类型(unique_pointer想到)在许多之前不容许使用的地方。例如,这些类型如今能够用做STL容器元素。

如今咱们知道了std::move,咱们能够看到为何我以前展现的复制赋值运算符的rvalue引用重载的 实现仍然有点问题。考虑变量之间的简单分配,以下所示:
a = b;
你指望在这里发生什么?你但愿将所持有的对象a替换为副本b,而且在替换过程当中,之前对象a保留的资源被破坏。如今考虑一下下边这句:
a = std :: move(b);
若是移动语义做为一个简单的交换来实现,那么这样作的效果是,经过持有的对象 a和b正在之间交换a和b。什么都没有被破坏。当b不在函数范围内时,之前由a所持有的对象固然最终会被破坏。固然,除非b再次被move,不然之前a所持有的对象就会被销毁。所以,就复制赋值算子的实现者而言,不知道之前保持的对象a什么时候被破坏。
因此从某种意义上说,咱们已经在这里进入了非肯定性毁灭的暗界:一个变量被分配给了(新内容),但以前由该变量持有的对象仍然存在于某个地方。只要对该物体的破坏没有外界可见的任何反作用, 这就能够了。但有时候析构函数会产生这样的反作用。一个例子是在析构函数中释放一个锁。所以, 对象销毁中具备反作用的任何部分都应在拷贝构造运算符的 rvalue 引用重载中显式执行:

X& X::operator=(X&& rhs)
{

  // Perform a cleanup that takes care of at least those parts of the
  // destructor that have side effects. Be sure to leave the object
  // in a destructible and assignable state.

  // Move semantics: exchange content between this and rhs
  
  return *this;
}

右值引用是右值吗?

和之前同样,让X成为一个类,咱们已经重载了复制构造函数和复制赋值运算符来实现移动语义。如今考虑:

void foo(X&& x)
{
  X anotherX = x;
  // ...
}

问题是:在foo函数中,X调用的是哪一个拷贝构造运算符的重载呢?这里 x 被声明为右值引用,一般,一个引用最好是右值(尽管也不是必须的)。所以指望x和一个右值绑定也是合理的,即: X(X&& rhs); 应该被调用。换句话说,人们可能指望任何被声明为右值引用的东西自己就是一个右值。右值引用的设计者选择了一个比这更微妙的解决方案:

声明为右值参考的 things 能够是左值或右值。区别标准是:若是它有一个名字,那么它就是一个左值。不然,它是一个右值。

在上面的例子中,声明为右值引用的东西有一个名称,所以它是一个左值:

void foo(X&& x)
{
  X anotherX = x; // calls  **X(X const & rhs)**
}

下边是一个被声明为右值引用但没有名称的例子,所以是一个右值:

X&& goo();    //对于我而言,须要注意。老是混淆:本行,本质是一个 goo() 函数返回的 X&& 的无名引用
X x = goo(); // calls **X(X&& rhs)** because the thing on
             // the right hand side has no name

如下是设计背后的基本原理:若是容许将移动语义默认应用于具备名称的内容,如

X anotherX = x;
  // x is still in scope!

中, 会形成危险的混乱和容易出错, 由于咱们刚刚移动的东西, 即咱们刚刚盗窃的东西, 仍然能够在后续的代码行中访问。但移动语义的所有意义在于只在 "不重要" 的地方应用它, 也就是说, 咱们移动的东西在移动后就会死亡并消失。所以有规则,“若是它有一个名字,那么它是一个左值。”

那么另外一部分呢,“若是它没有名字,那么它是一个右值?” 查看上面goo的示例,从技术上讲,示例第二行中的表达式 goo()所引用的内容在移动以后仍可访问,这是可能的,尽管不太可能。但回想一下上一节:有时候这就是咱们想要的!咱们但愿可以根据本身的判断强制在左值上移动语义,而且正是规则“若是它没有名称,那么它是一个rvalue”容许咱们以受控的方式实现它。这就是函数的std::move 工做原理。尽管如今向您展现确切的实施还为时尚早,但咱们距离理解std::move还有一步之遥。它经过引用直接传递它的参数,根本不执行任何操做,其结果类型是右值引用。因此表达式std::move(x) 被声明为一个右值引用,没有名称。所以,它是一个右值,所以, std::move "将其参数转换为 rvalue, 即便它不是,", 它经过 "隐藏名称" 来实现这一点。

假设您已经编写了一个类Base,而且您已经经过重载Base的复制构造函数和赋值运算符实现了移动语义:

Base(Base const & rhs); // non-move semantics
Base(Base&& rhs); // move semantics

你编写一个Derived派生自的类Base。为了确保将移动语义应用于对象的Base一部分,您Derived还必须重载Derived复制构造函数和赋值运算符。咱们来看看复制构造函数。相似地处理复制赋值运算符。左值的版本很简单:

Derived(Derived const & rhs) 
  : Base(rhs)
{
  // Derived-specific stuff
}

rvalues的版本有一个很大的微妙。如下是不了解if-it-a-name规则的人 可能作过的事情:

Derived(Derived&& rhs) 
  : Base(rhs) // wrong: rhs is an lvalue
{
  // Derived-specific stuff
}

若是咱们这样编码,Base那么将调用非移动版本的复制构造函数,由于rhs具备名称的是左值。咱们想要被称为Base移动复制构造函数,得到它的方法是编写

Derived(Derived&& rhs) 
  : Base(std::move(rhs)) // good, calls Base(Base&& rhs)
{
  // Derived-specific stuff
}

移动语义和编译器优化

考虑如下函数定义:

X foo()
{
  X x;
  // perhaps do something to x
  return x;
}

如今假设像之前同样,X是一个类,咱们已经重载了复制构造函数和复制赋值运算符来实现移动语义。若是你将上面的函数用于定义一个值,你可能会想说,等一下,这里有一个值复制,从x到foo返回值的位置。让我确保咱们使用移动语义代替:

```cpp
X foo()
{
X x;
// perhaps do something to x
return std::move(x); // making it worse!
}
···

不幸的是,这会让事情变得更糟而不是更好。任何现代编译器都会将 返回值优化应用于原始函数定义。换句话说,x不是编译器在本地构造而后将其复制出来,而是直接在foo返回值的位置构造对象。显然,这甚至比移动语义更好。

完美的转发:问题

除了 rvalue 引用设计要解决的移动语义以外, 另外一个问题是完美的转发问题。请考虑如下简单的工厂功能:

template<typename T, typename Arg> 
shared_ptr<T> factory(Arg arg)
{ 
  return shared_ptr<T>(new T(arg));
}

显然, 这里的目的是将参数 arg 从工厂函数转发到 t 的构造函数。理想状况下, 就 arg 而言, 一切都应该表现得就像工厂函数不存在, 构造函数直接在客户端代码中调用: 完美转发。上面的代码在这一点上失败得很惨: 它引入了一个额外的按值的调用, 若是构造函数经过引用获取它的参数,这个(按值的调用)特别糟糕。
最多见的解决方案,例如boost::bind,经过引用让外部函数接受参数:

emplate<typename T, typename Arg> 
shared_ptr<T> factory(Arg& arg)
{ 
  return shared_ptr<T>(new T(arg));
}

这样更好,但并不完美。问题是如今,没法在rvalues上调用工厂函数:

factory<X>(hoo()); // error if hoo returns by value
factory<X>(41); // error

这能够经过提供一个重载来修复,该重载经过const引用获取其参数:

template<typename T, typename Arg> 
shared_ptr<T> factory(Arg const & arg)
{ 
  return shared_ptr<T>(new T(arg));
}

这种方法存在两个问题。首先,若是factory不是一个,但有几个参数,则必须为各类参数的非const和const引用的全部组合提供重载。所以,该解决方案对具备多个参数的函数的扩展性极差。其次, 这种转发不是十全十美的, 由于它阻止了移动语义: 工厂主体中 T 构造函数的参数是一个值。所以, 即便没有包装函数, 也永远不会发生移动语义。
事实证实,右值引用可用于解决这两个问题。它们能够在不使用重载的状况下实现真正完美的转发。为了理解如何,咱们须要再考虑另外两个rvalue引用规则。

完美的转发:解决方案

rvalue 引用的其他两个规则中的第一个规则也会影响旧式的 lvalue 引用。回想一下, 在 11 c++ 以前, 不容许引用引用: 相似 a & & 会致使编译错误。相比之下, C++11 引入了如下引用折叠规则:

  • A& & becomes A&
  • A& && becomes A&
  • A&& & becomes A&
  • A&& && becomes A&&

其次,函数模板有一个特殊的模板参数推导规则,它经过对模板参数的rvalue引用来获取参数:

template<typename T>
void foo(T&&);

在这里,如下适用:
*当 foo 在 A 型的 lvalue 上被调用时, T 解析为 A &, 所以, 经过上面的引用折叠规则, 参数类型有效地成为A&

  • 当在 A 型的 rvalue 上调用 foo 时,T解析为 A, 所以参数类型变为 A&&
    根据这些规则,咱们如今可使用rvalue引用来解决上一节中提出的完美转发问题。这是解决方案的样子:
template<typename T, typename Arg> 
shared_ptr<T> factory(Arg&& arg)
{ 
  return shared_ptr<T>(new T(std::forward<Arg>(arg)));
}

std::forward 定义以下:

template<class S>
S&& forward(typename remove_reference<S>::type& a) noexcept
{
  return static_cast<S&&>(a);
}

为了了解上面的代码是如何实现完美转发的, 咱们将分别讨论当咱们的工厂函数在 lvalues 和 rvalues 上被调用时会发生什么。让 A 和 X 做为类型。假设首先在 类型位为X的 lvalue 上调用factory<A>

X x;
factory<A>(x);

而后,经过上面提到的特殊模板推导规则,将factory模板参数Arg解析为X&。所以,编译器将建立如下实例:factorystd::forward

shared_ptr<A> factory(X& && arg)
{ 
  return shared_ptr<A>(new A(std::forward<X&>(arg)));
} 

X& && forward(remove_reference<X&>::type& a) noexcept
{
  return static_cast<X& &&>(a);
}

在评估remove_reference并应用参考折叠规则后,这将变为:

shared_ptr<A> factory(X& arg)
{ 
  return shared_ptr<A>(new A(std::forward<X&>(arg)));
} 

X& std::forward(X& a) 
{
  return static_cast<X&>(a);
}

这确定是左值的完美转发:arg工厂函数的参数A经过两个间接级别传递给构造函数,二者都是经过老式的左值引用。

接下来,假设在类型为X的右值调用factory<A>

X foo();
factory<A>(foo());

而后,再次经过上面提到的特殊模板推导规则,将factory模板参数Arg解析为X。所以,编译器如今将建立如下函数模板实例化:

shared_ptr<A> factory(X&& arg)
{ 
  return shared_ptr<A>(new A(std::forward<X>(arg)));
} 

X&& forward(X& a) noexcept
{
  return static_cast<X&&>(a);
}

这确实是rvalues的完美转发:工厂函数的参数A经过引用的两个间接层传递给构造函数。此外,A的构造函数将其声明为一个表达式,该表达式被声明为右值引用,而且没有名称。根据 无名称规则,这样的事情是一个右值。所以, A在rvalue上调用构造函数。这意味着转发会保留了工厂包装器不存在时可能发生的任何移动语义。
值得注意的是,保留移动语义其实是std :: forward在这种状况下的惟一目的。若是不使用std :: forward,一切都会很好地工做,一切都会很好地工做, 只是 a 的构造函数老是会将具备名称的东西视为其参数, 而这样的东西就是一个 lvalue。另外一种说法就是说std​​ :: forward的目的是转发信息,不管是在调用点包装器看到左值,仍是右值。
为何须要std :: forward定义中的remove_reference? 答案是,根本不须要它。若是您在std :: forward的定义中只使用S&而不是remove_reference <S> :: type&,您能够重复上面的案例区分写区分来讲服本身完美转发仍然能够正常工做。可是,只要咱们明确指定Arg做为std :: forward的模板参数,它就能够正常工做。 std :: forward定义中remove_reference的目的是强迫咱们这样作。
咱们快作完了。只剩下研究std:: move的实现状况了。请记住, std:: move的目的是经过引用直接传递它的参数, 并使其像 rvalue 同样绑定。下面是实现:

template<class T> 
typename remove_reference<T>::type&&
std::move(T&& a) noexcept
{
  typedef typename remove_reference<T>::type&& RvalRef;
  return static_cast<RvalRef>(a);
}

假设咱们调用std::move了一个类型的左值X:

X x;
std::move(x);

经过新的特殊模板推导规则,模板参数T将解析为X&。所以,编译器最终实例化的是:

typename remove_reference<X&>::type&&
std::move(X& && a) noexcept
{
  typedef typename remove_reference<X&>::type&& RvalRef;
  return static_cast<RvalRef>(a);
}

在评估remove_reference并应用新的参考折叠规则后,这就变成了:

X&& std::move(X& a) noexcept
{
  return static_cast<X&&>(a);
}

这样作:咱们的左值x将绑定到做为参数类型的左值引用,函数将其直接传递,将其转换为未命名的右值引用。
我留给你说服本身std::move在rvalue上调用时实际上工做正常。可是你可能想要跳过这个:为何有人想要调用std::move 右值,当它的惟一目的是将事物变成右值?此外,你如今可能已经注意到了,而不是:

std::move(x);

你也能够写 static_cast<X&&>(x); 然而,std::move强烈优选,由于它更具表现力。

相关文章
相关标签/搜索