右值引用、移动语义、完美转发

前言

右值引用、移动语义、完美转发是 C++ 11 引入的一大新特性。即使在代码中没有显示地使用到他们我们也能得到性能上的提升,STL 中容器类就大量使用了这些新特性,在使用这些容器时我们”隐式“地从中受益。比如 vector 类在扩容时需要把原来的对象挪到新开辟的空间中,若数据类型重写了移动构造函数并且标记为 noexcept 就可以将旧对象移动过去而不是使用拷贝构造的形式,就如 C++ Primer Plus 中所说:“对大多数程序员来说,右值引用带来的主要好处并非是让他们能够编写使用右值引用的代码,而是能够使用利用右值引用实现移动语义的库代码”。

右值引用与移动语义

先说关系,右值引用的提出是为了实现移动语义,即移动语义是目的,右值引用是手段。

  • 对于左值与右值一种简单的理解是左值是可以取地址的而右值不可以,右值引用是 C++ 11 中引入的新类型,用于标识字面常量(1,“string”)、表达式(x+y)和函数的按值返回。
  • 移动语义实现了将一个右值对象的资源转移到当前对象,避免了拷贝构造的开销;在序列容器类的扩容以及两个对象的 swap 等情况极大提高了效率。
  • 移动语义主要体现在为某类重写的移动构造函数中,在实现中一般是进行指针值的拷贝然后置空右值对象中对应的指针。进行指针值的拷贝也即浅拷贝,从这点上看移动语义与浅拷贝有点相似,但实际上二者是两个不同的概念:浅拷贝是共享资源,而 move 是独占资源(窃取后置空原指针),浅拷贝因共享资源从而可能引发重复析构的问题,而 move 是独占则不会。

万能引用

what:即可以接受左值引用也可以接受右值引用的参数类型,写作 T&&,注意只有当发生自动类型推断(如函数模板的类型自动推导,或 auto 关键字)时 T&& 才表示万能引用,否则只表示右值引用

how:通过引用折叠实现(只有右值引用的右值引用会折叠成右值引用,其余情况都是折叠成左值引用):

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

关于万能引用在我另一篇博客中有提到。

完美转发(perfect forwarding)

为什么需要完美转发的原因:右值引用本身是一个左值,如将一个右值引用参数传递给某函数 f1,而在该函数中又将该参数传给另一个函数 f2,f2 会在进行实参形参的匹配时会将该参数视为左值,而我们希望无论如何转发该参数都能保持其为右值对象这一特点,因此需要完美转发:通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值,可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。实现上要借助’万能引用’和 std::forward

分析实现代码之前先看看 std::remove_reference

std::remove_reference

std::forward 的实现中用到了 std::remove_reference。std::remove_reference 是 type_traits(类型萃取)之一,由类模板实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1
template<typename _Tp>
struct remove_reference
{ typedef _Tp type; };

// 2
template<typename _Tp>
struct remove_reference<_Tp&>
{ typedef _Tp type; };

// 3
template<typename _Tp>
struct remove_reference<_Tp&&>
{ typedef _Tp type; };

1 接受原始类型,2 、3 作为 1 的特化接受 lvalue-ref、 rvalue-ref。当 remove_reference 模板实例化后该模板中定义的类型 type 就具化为模板参数的原始类型

std::forward 代码解析

std::forward 是一个函数模板,针对转发左值和转发右值有不同的重载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 转发左值
template<typename _Tp>
constexpr _Tp&&
forward(typename std::remove_reference<_Tp>::type& __t) noexcept
{ return static_cast<_Tp&&>(__t); }

// 转发右值
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);
}
  • 返回值类型:都是 constexpr _Tp&&,常量表达式可在编译期确定,_Tp&& 是一个万能引用。
  • 接收参数:用 remove_reference 得到参数的原始类型;转发左值版本给参数加上左值引用,转发右值版本给参数加上右值引用。
  • 内部实现:两个重载的实现上基本一致,都利用 static_cast 将参数转化为左值引用或者右值引用,返回语句都是 return static_cast<_Tp&&>(__t);,这里用到了引用折叠的性质。略有不同的是转发右值的重载中多了一个 static_assert 在编译时避免将右值引用 forward 为左值引用。刚开始看到转发右值的版本中有 static_assert 而转发左值的版本中没有时还不太理解,后来在 Stack Overflow 上找到了一个类似的提问,转发右值的版本中的 static_assert 是为了避免类似 forward<string&>(string()))的情况。即不允许将一个“纯右值”转发为左值;但是允许将左值转发为右值,实际上这正是 ‘std::move()’ 做的事情。这其实是设计理念方面的问题,具体参见这里

参考