C++11:std::move和std::forward源码分析

std::move和std::forward是C++0x中新增的标准库函数,分别用于实现移动语义和完美转发。
下面让我们分析一下这两个函数在gcc4.6中的具体实现。

预备知识

  1. 引用折叠规则:
    X& + & => X&
    X&& + & => X&
    X& + && => X&
    X&& + && => X&&

  2. 函数模板参数推导规则(右值引用参数部分):
    当函数模板的模板参数为T而函数形参为T&&(右值引用)时适用本规则。
    若实参为左值 U& ,则模板参数 T 应推导为引用类型 U& 。
    (根据引用折叠规则, U& + && => U&, 而T&& ≡ U&,故T ≡ U& )
    若实参为右值 U&& ,则模板参数 T 应推导为非引用类型 U 。
    (根据引用折叠规则, U或U&& + && => U&&, 而T&& ≡ U&&,故T ≡ U或U&&,这里强制规定T ≡ U )

  3. std::remove_reference为C++0x标准库中的元函数,其功能为去除类型中的引用。
    std::remove_reference::type ≡ U
    std::remove_reference::type ≡ U
    std::remove_reference::type ≡ U

  4. 以下语法形式将把表达式 t 转换为T类型的右值(准确的说是无名右值引用,是右值的一种)
    static_cast(t)

  5. 无名的右值引用是右值
    具名的右值引用是左值。

  6. 注:本文中 ≡ 含义为“即,等价于“。

std::move

函数功能

std::move(t) 负责将表达式 t 转换为右值,使用这一转换意味着你不再关心 t 的内容,它可以通过被移动(窃取)来解决移动语意问题。

源码与测试代码

  template
    inline typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t)
    { return static_cast::type&&>(__t); }
#include
using namespace std;
 
struct X {};
 
int main()
{
	X a;
	X&& b = move(a);
	X&& c = move(X());
}

代码说明

  1. 测试代码第9行用X类型的左值 a 来测试move函数,根据标准X类型的右值引用 b 只能绑定X类型的右值,所以 move(a) 的返回值必然是X类型的右值。

  2. 测试代码第10行用X类型的右值 X() 来测试move函数,根据标准X类型的右值引用 c 只能绑定X类型的右值,所以 move(X()) 的返回值必然是X类型的右值。

  3. 首先我们来分析 move(a) 这种用左值参数来调用move函数的情况。

  4. 模拟单步调用来到源码第3行,_Tp&& ≡ X&, __t  ≡ a 。

  5. 根据函数模板参数推导规则,_Tp&& ≡ X& 可推出 _Tp ≡ X& 。

  6. typename std::remove_reference<_Tp>::type ≡ X 。
    typename std::remove_reference<_Tp>::type&& ≡ X&& 。

  7. 再次单步调用进入move函数实体所在的源码第4行。

  8. static_cast::type&&>(__t) ≡ static_cast(a)

  9. 根据标准 static_cast(a) 将把左值 a 转换为X类型的无名右值引用。

  10. 然后我们再来分析 move(X()) 这种用右值参数来调用move函数的情况。

  11. 模拟单步调用来到源码第3行,_Tp&& ≡ X&&, __t  ≡ X() 。

  12. 根据函数模板参数推导规则,_Tp&& ≡ X&& 可推出 _Tp ≡ X 。

  13. typename std::remove_reference<_Tp>::type ≡ X 。
    typename std::remove_reference<_Tp>::type&& ≡ X&& 。

  14. 再次单步调用进入move函数实体所在的源码第4行。

  15. static_cast::type&&>(__t) ≡ static_cast(X())

  16. 根据标准 static_cast(X()) 将把右值 X() 转换为X类型的无名右值引用。

  17. 由9和16可知源码中std::move函数的具体实现符合标准,
    因为无论用左值a还是右值X()做参数来调用std::move函数,
    该实现都将返回无名的右值引用(右值的一种),符合标准中该函数的定义。

std::forward

函数功能

std::forward(u) 有两个参数:T 与 u。当T为左值引用类型时,u将被转换为T类型的左值,否则u将被转换为T类型右值。如此定义std::forward是为了在使用右值引用参数的函数模板中解决参数的完美转发问题。

源码与测试代码

  /// forward (as per N3143)
  template
    inline _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) 
    { return static_cast<_Tp&&>(__t); }
 
  template
    inline _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) 
    {
      static_assert(!std::is_lvalue_reference<_Tp>::value, "template argument"
		    " substituting _Tp is an lvalue reference type");
      return static_cast<_Tp&&>(__t);
    }
#include
using namespace std;
 
struct X {};
void inner(const X&) {cout << "inner(const X&)" << endl;}
void inner(X&&) {cout << "inner(X&&)" << endl;}
template
void outer(T&& t) {inner(forward(t));}
 
int main()
{
	X a;
	outer(a);
	outer(X());
	inner(forward(X()));
}
//inner(const X&)
//inner(X&&)
//inner(X&&)

代码说明

  1. 测试代码第13行用X类型的左值 a 来测试forward函数,程序输出表明 outer(a) 调用的是 inner(const X&) 版本,从而证明函数模板outer调用forward函数在将参数左值 a 转发给了inner函数时,成功地保留了参数 a 的左值属性。

  2. 测试代码第14行用X类型的右值 X() 来测试forward函数,程序输出表明 outer(X()) 调用的是 inner(X&&) 版本,从而证明函数模板outer调用forward函数在将参数右值 X() 转发给了inner函数时,成功地保留了参数 X() 的右值属性。

  3. 首先我们来分析 outer(a) 这种调用forward函数转发左值参数的情况。

  4. 模拟单步调用来到测试代码第8行,T&& ≡ X&, t  ≡ a 。

  5. 根据函数模板参数推导规则,T&& ≡ X& 可推出 T ≡ X& 。

  6. forward(t) ≡ forward(t),其中 t 为指向 a 的左值引用。

  7. 再次单步调用进入forward函数实体所在的源码第4行或第9行。

  8. 先尝试匹配源码第4行的forward函数,_Tp ≡ X& 。

  9. typename std::remove_reference<_Tp>::type ≡ X 。
    typename std::remove_reference<_Tp>::type& ≡ X& 。

  10. 形参 __t  与实参 t 类型相同,因此函数匹配成功。

  11. 再尝试匹配源码第9行的forward函数,_Tp ≡ X& 。

  12. typename std::remove_reference<_Tp>::type ≡ X 。
    typename std::remove_reference<_Tp>::type&& ≡ X&& 。

  13. 形参 __t  与实参 t 类型不同,因此函数匹配失败。

  14. 由10与13可知7单步调用实际进入的是源码第4行的forward函数。

  15. static_cast<_Tp&&>(__t) ≡ static_cast(t) ≡ a。

  16. inner(forward(t)) ≡ inner(static_cast(t)) ≡ inner(a) 。

  17. outer(a) ≡ inner(forward(t)) ≡ inner(a)
    再次单步调用将进入测试代码第5行的inner(const X&) 版本,左值参数转发成功。

  18. 然后我们来分析 outer(X()) 这种调用forward函数转发右值参数的情况。

  19. 模拟单步调用来到测试代码第8行,T&& ≡ X&&, t  ≡ X() 。

  20. 根据函数模板参数推导规则,T&& ≡ X&& 可推出 T ≡ X 。

  21. forward(t) ≡ forward(t),其中 t 为指向 X() 的右值引用。

  22. 再次单步调用进入forward函数实体所在的源码第4行或第9行。

  23. 先尝试匹配源码第4行的forward函数,_Tp ≡ X 。

  24. typename std::remove_reference<_Tp>::type ≡ X 。
    typename std::remove_reference<_Tp>::type& ≡ X& 。

  25. 形参 __t  与实参 t 类型相同,因此函数匹配成功。

  26. 再尝试匹配源码第9行的forward函数,_Tp ≡ X 。

  27. typename std::remove_reference<_Tp>::type ≡ X 。
    typename std::remove_reference<_Tp>::type&& ≡ X&& 。

  28. 形参 __t  与实参 t 类型不同,因此函数匹配失败。

  29. 由25与28可知22单步调用实际进入的仍然是源码第4行的forward函数。

  30. static_cast<_Tp&&>(__t) ≡ static_cast(t) ≡ X()。

  31. inner(forward(t)) ≡ inner(static_cast(t))  ≡ inner(X())。

  32. outer(X()) ≡ inner(forward(t)) ≡ inner(X())
    再次单步调用将进入测试代码第6行的inner(X&&) 版本,右值参数转发成功。

  33. 由17和32可知源码中std::forward函数的具体实现符合标准,
    因为无论用左值a还是右值X()做参数来调用带有右值引用参数的函数模板outer,
    只要在outer函数内使用std::forward函数转发参数,
    就能保留参数的左右值属性,从而实现了函数模板参数的完美转发。

自己的思考

Flag:以后转载文章都要有这个章节!

当时想既然有remove_reference了,为何move函数参数不能直接用T或者T&就好?后来发现这个问题比较蠢:

  1. 直接T会有warning,因为这里相当于把对象浅拷贝了一份,__t是本地变量,在退出函数时就会销毁,所以warning告诉你这里可能会undefined。
  2. 使用T&,更简单,直接报错了,因为第二个测试语句传的值X()是右值,而修改之后函数参数T&期望的是左值引用,所以不匹配。

forward的存在是为了解决无法“完美转发”(在传参时保持原来的参数类型)的问题,即通过一层参数传递之后的右值会变成左值(具名右值为左值),而由于引用坍缩,以右值参数作为参数的函数也能接收左值参数(其实在move里已经体现了)。

原文forward函数写了两个重载,但是实际上第二个接收右值参数的不符合实际场景(没有中间层,没有具名右值)只是备用

  1. 当中间层接收左值参数时,由于模板匹配,_Tp ≡ T ≡ X&(参数类型推导) ,所以_Tp&& ≡ X&(引用坍缩),返回值为左值引用;
  2. 当中间层接收右值参数时,_Tp ≡ T ≡ X/X&&,所以_Tp&& ≡ X&&,返回值变为了右值引用。

巧妙核心之处就在于利用了函数参数类型推导对于左右值的差异,将这种差异体现在模板类型推导中,进而造成返回值的类型差异。

我自己用于测试的源码:https://gist.github.com/dc3671/2c7a0ea56f7284e3d33ba0b78e8f8b0f

转载于:https://my.oschina.net/dc3671/blog/3029702

你可能感兴趣的:(C++11:std::move和std::forward源码分析)