从源码分析C++中forward完美转发和move移动语义的本质区别

完美转发可以看做一种能够按照原来类型转发到另一个地方(函数)的方法(废话。。)

咱不如直接上源代码(move.h):

  template<typename _Tp>
    _GLIBCXX_NODISCARD
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type& __t) noexcept
    { return static_cast<_Tp&&>(__t); }

  /**
   *  @brief  Forward an rvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    _GLIBCXX_NODISCARD
    constexpr _Tp&&
    forward(typename std::remove_reference<_Tp>::type&& __t) noexcept
    {
      static_assert(!std::is_lvalue_reference<_Tp>::value,
	  "std::forward must not be used to convert an rvalue to an lvalue");
      return static_cast<_Tp&&>(__t);
    }

  /**
   *  @brief  Convert a value to an rvalue.
   *  @param  __t  A thing of arbitrary type.
   *  @return The parameter cast to an rvalue-reference to allow moving it.
  */
  template<typename _Tp>
    _GLIBCXX_NODISCARD
    constexpr typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t) noexcept
    { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }

必须承认,C++源代码,尤其是含模板的代码看起来十分头疼。

其实粗略浏览后可以发现2个forward和1个move的函数体都十分的简短,其中,move直接传入右值引用,而forward有右值和左值引用(不知道什么是右值?那为了您的身心健康还是别往下看了)。众所周知,右值引用只能右值,但左值引用可以在const的限制下引用右值(不过左值可以用static_cast转换为右值,这个后面会用到),所以对于forward来说左值实参会调用& __t版本,右值实参则是&&版本,而move只有一种调用。库中的两种forward及move全都不约而同的返回右值的引用(至少现在看起来是这样),根据上述特点我们写出下面的实验代码:

#include <iostream>
using namespace std;

template<typename T>
void put_l_or_r(T& t){
    cout << "lvalue" << endl;
}
template<typename T>
void put_l_or_r(T&& t){
    cout << "rvalue" << endl;
}

template<typename T>
void testfunc(T && v){
    put_l_or_r(v);
    put_l_or_r(std::forward<T>(v));
    put_l_or_r(std::move(v));
}

int main(){
    int x = 520;
    testfunc(114514);
    testfunc(x);
    testfunc(std::forward<int>(x));
    return 0;
}

其中main中的三个testfunc调用都可以直接调用右值引用版本(当然我也只提供了这个版本),testfunc中分别调用三次不同的put_l_or_r,其中第三个testfunc传入的是一个forward函数返回的int&&。由于实参为左值,所以这里理所应当调用第一个forward即左值引用的形参函数,这没什么特别之处,但实例化的情况与move完全不同(后面会讲到),它们的输出结果如下:

可能令人感到奇怪的第一点是,第一个put输出的永远是lvalue,也就是说虽然testfunc函数形参被初始化的是右值引用,但编译器似乎自动的将其当做左值处理。事实上,把鼠标悬停以查看三个testfunc函数调用的原型发现第一和第三个调用的原型为void testfunc<int>(int &&v),而第二个调用原型却是void testfunc<int &>(int &v),着重注意这里的模板实例化分别是int和int &,也就是说后者的右值引用符号&&直接被吞掉了。

稍作思考后我们可以发现,虽然类型规定的的确是&&,但在应用层面来讲这并不合适。不管传入的实参是右值还是左值,都不应影响函数内将其作为什么值,想象函数被初始化这样一个右值引用的形参,我们真实的目的是想要将其作为右值来使用吗?回想设计右值引用的原始目的,是想在C++中提供移动语义,如move函数和移动系列类方法,但我们想要的只是在想要使用移动语义时才使用它,而非随时随地都将其作为右值引用进而调用移动语义,所以编译器将右值引用形参在作为其它函数参数时直接当做左值引用以方便我们正常使用。如此一来,我们的3个testfunc函数都是将形参当做左值来传递给其它函数了。

紧接着的第二个奇怪点是,第二个testfunc的输出与其它两个有所不同,唯一的区别在于forward版本输出的是lvalue而非rvalue,即forward函数似乎返回的是左值引用而非右值引用,wait,前面我们分析了testfunc会将形参作为左值引用来使用,怎么这里3个testfunc的输出会有所不同呢?这个不同点的触发原因应该是我们传入的实参为左值,显然这其中有更深层次的奥秘。

我们不妨回到源代码去观察,先看move函数,由于它只有一个模板,所以无论左值还是右值都只会调用右值引用版本,而无论形参为什么引用,最后都会被static_cast<typename std::remove_reference<_Tp>::type&&>(__t); 这句代码转换为右值引用。其中typename仅仅表示后面的type是一个类型名而非实例名,remove_reference仅仅只是包含一句typedef _Tp type; 的结构体罢了。事实上它有直接值、左值和右值3种模板版本,这样在使用其定义的type类型时可以直接解除传入类型的左值或右值引用,即remove reference解引用的意义。这里需要使用它的原因是如果传入move的实参为左值引用,则move的模板类型参数会被初始化为int &,如果直接使用static_cast<_Tp>&&>(__t),则会转换为左值引用的右值引用(什么乱七八糟的)。

现在我们明白move函数返回一个右值引用,它一定会导致调用put_l_or_r的右值引用版本,那么forward有何如呢?

事实上,如果你还记得前所说的右值引用只能绑定右值这条规定的话,你应该早已感到费解,因为第二个testfunc函数居然直接传入的左值,而我们只有右值引用的函数模板啊,这是怎么回事?不会报错?(当然不会,否则还搁这儿写啥呢)其实我们之前提到过第二个testfunc的调用原型是void testfunc<int &>(int &v),也就是&&符号被吃掉了(或许是remove_reference的语法糖?),这样看来模板函数中的右值引用是可以引用左值的。如果testfunc的类型参数T为int,则编译器会根据实参为左值还是右值调用第一或二个forward版本并通过static_cast<_Tp&&>(__t)返回一个右值引用。但如果是上述的int&呢?由于forward的参数类型被实例化为int&,且传入的实参v也为int&,所以理所应当的调用第一个forward,但注意到函数体中的语句static_cast<_Tp&&>(__t)以及函数的返回值constexpr _Tp&&,如果将_Tp替换为int&,那将是绝杀,因为这变成了int& &&即引用的引用。引用可不像指针可以搞多重指针,C++不允许多重引用,所以这是有问题的。我的推测依旧是编译器自动使用了remove_reference语法糖,将_Tp&&转为了_Tp,进而返回左值引用,使得最终调用第一个put_l_or_r函数。(maybe。。)

通过上述分析,其实我们可以发现forward实现完美转发的本质是使用显式实例化的模板类型参数,当为_Tp为int时无论传参为左值还是右值,返回值都为右值引用;而当_Tp为int&时无论传参为左值还是右值,返回值都为左值引用。我们甚至可以写出forward<int&>((int&&)x)这样的语句以及forward<int&&>(x)这样的家伙!它们都在当模板中有诸如引用的右值引用时将右值引用的符号&&给剔除了,这样的话前者返回int&,后者返回int&&。(其实第一句会报错,因为forward的右值引用版本中有一个static_assert,后面会讲到)

讲了许多,自己都有点搞不清楚了(南泵),剩下的篇幅,讲讲源码中的一些可疑的词语。在forward函数返回值前有一个_GLIBCXX_NODISCARD,它被扩展到[[__nodiscard__]],这表明使用该函数的人应该确切使用到函数的返回值,如果没有使用到返回值则会产生编译警告。

static_assert是编译期断言,使用static这个词语也就意味着需要编译期判断,第一个参数是常量表达式,必须在编译期能够给出值的表达式,像这里的std::is_lvalue_reference<_Tp>::value其实是判断_Tp是否为左值引用。如前所述,当我们写出forward<int&>((int&&)x)这样的调用时,forward会将_Tp设为int&,而由于传参为int&&,所以会调用第二个forward即右值引用版本,注意到此时的is_lvalue_reference被设为了is_lvalue_reference<int&>,根据其定义(自己去康康),value的值会设为true,那么源码中的static_assert断言的第一个参数就会为false,这将使得断言失效,触发编译error并显示出第二个字符串参数的内容,即"std::forward must not be used to convert an rvalue to an lvalue"。这里很明确的说明了forward不能将右值转为左值,这和我们前面分析的会返回int&是一致的,只是这里被编译器阻止了。其实并不是语法上不能转换,而是编译器认为我们不应该将左值转换为右值,将这种操作视为失误的使用forward而报错提醒使用者,事实上将右值转为左值当然是不合理的,因为右值很可能是将亡值,将其转为左值继续使用不就是在玩火吗?(咱C系语言就喜欢玩火)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值