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

C ++ Rvalue引用说明

以下内容,主要是上述链接的摘要

介绍

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

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

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

// 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,可以通过重载实现此条件行为:

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&&)

重点是:

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返回值的位置。让我确保我们使用移动语义代替:

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

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

## 完美的转发:问题

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

```cpp
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强烈优选,因为它更具表现力。

原文地址:https://www.cnblogs.com/gardenofhu/p/10077348.html