c++完美转发详解

1.1 引言

在学习右值引用时,对于完美转发的功能一直理解不透彻,然后在网上看到这样一个例子,注释后面是实际结果。这个demo实际上就引出了为什么要使用完美转发,从打印结果可以看出来,不管forward()里面传的是左值引用还是右值引用,进入forward函数内部时,i都被转换为了左值,导致实际forward()内部调用process()时,都是调用了左值引用入参的形式。完美转发就是问了解决这样一个场景,但这个demo却是个极容易误导新手的例子,后面我们慢慢展开来说。

#include<iostream>
#include <cstring>
#include <vector>

using namespace std;
void process(int& i) {
  std::cout << "process(&) " << i << std::endl;
}

void process(int&& i) {
  std::cout << "process(&&) " << i << std::endl;
}

void forward(int&& i) {
  std::cout << "forward(&&) " << i << std::endl;
  process(i);
}


int main(int argc, char *argv[])
{
  int c = 0;
  process(c);                     // process(&) 0
  process(1);                     // process(&&) 1
  process(std::move(c));          // process(&&) 0

  forward(2);                     // forward(&&) 2  process(&) 2
  forward(std::move(c));          // forward(&&) 0  process(&) 0


  return 0;
}

现在我们从完美转发的功能出发,再根据实测结果一步步剥开完美转发的面纱,适合和我一样看了半天完美转发定义和作用却还是不明白其功能的读者。
完美转发作用:确保转发过程中引用的类型不发生任何改变,左值引用转发后一定还是左值引用,右值引用转发后一定还是右值引用!
按照这个上述的说法,如果我这样写,是不是就可以按照定义说的,forward传入左值,就转发左值引用到process,如果传递右值引用或者右值,就转发右值引用到process。

1.2 std::forward<int>功能初探

void process(int& i) {
  std::cout << "process(&) " << i << std::endl;
}

void process(int&& i) {
  std::cout << "process(&&) " << i << std::endl;
}

void forward(int&& i) {
  std::cout << "forward(&&) " << i << std::endl;
  process(std::forward<int>(i));
}


void forward(int& i) {
  std::cout << "forward(&) " << i << std::endl;
  process(std::forward<int>(i));
}


int main(int argc, char *argv[])
{
  int c = 0;
  forward(2);                     // forward(&&) 2  process(&&) 2 OK!!!
  forward(std::move(c));          // forward(&&) 0  process(&&) 0 OK!!!
  forward(c);          // forward(&) 0  process(&&) 0   ERROR!!!

  return 0;
}

相比起网上的例子,这里新增了一个函数void forward(int& i)用于接收一个左值的场景,按照完美转发的定义,这里似乎已经把第一个例子中的问题解决了,在 void forward(int&& i) 中,已经可以保证传入右值时,转发到process的也是右值;那我们再测试下左值的情况,发现好像和预期不符合,void forward(int& i)传入左值后,转发到process()中调用的还是入参为右值引用的函数,那这个std::forward和std::move不是没区别了吗,都是将一个值转换为右值引用。
实际上网上举的第一个例子就把我们带入到一个误区,因为完美转发必须使用在模板实例化的过程中,而上述的例子都是非模板函数中。下面我们通过模板来举例看看完美转发是否能解决函数传递参数过程中,潜移默化的将左值和右值都转换为左值的问题
改写下第一个demo,将所有函数都改为模板函数

#include<iostream>
#include <cstring>
#include <vector>

using namespace std;
template<class T>
void process(T& i) {
  std::cout << "process(&) " << i << std::endl;
}

template<class T>
void process(T&& i) {
  std::cout << "process(&&) " << i << std::endl;
}

template<class T>
void forward(T&& i) {
  std::cout << "forward(&&) " << i << std::endl;
  process(std::forward<T>(i));
}


int main(int argc, char *argv[])
{
  int c = 0;
  forward(2);                     // forward(&&) 2  process(&&) 2
  forward(std::move(c));          // forward(&&) 0  process(&&) 0
  forward(c);          // forward(&&) 0  process(&) 0

  return 0;
}

这时候看打印结果,已经达到完美转发预期的功能了,forward(c)传入左值后,转发到process()仍然是左值,forward(2)传入右值后,转发到process()仍然是右值,到这一步,似乎终于能理解完美转发的功能了,但std::forward<T>(i)具体做了什么呢,内部实际上就是做了一个static_cast的强转。

1.3 std::forward<int>内部实现

  /**
   *  @brief  Forward an lvalue.
   *  @return The parameter cast to the specified type.
   *
   *  This function is used to implement "perfect forwarding".
   */
  template<typename _Tp>
    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>
    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);
    }

那这里又有问题了,一个强转是怎么实现上述功能的呢,static_cast<_Tp&&>看起来还是在把一个值强转成右值啊;除非。。这个转换过程不是发生在函数内部,而是在函数外部就已经判断出来了_Tp是左值还是右值?
继续改写下上面demo,将std::forward<T>(i)替换成static_cast<T>(i),看看是否依然可以实现完美转发?

#include<iostream>
#include <cstring>
#include <vector>

using namespace std;
template<class T>
void process(T& i) {
  std::cout << "process(&) " << i << std::endl;
}

template<class T>
void process(T&& i) {
  std::cout << "process(&&) " << i << std::endl;
}

template<class T>
void forward(T&& i) {
  std::cout << "forward(&&) " << i << std::endl;
  process(static_cast<T>(i));
}


int main(int argc, char *argv[])
{
  int c = 0;
  forward(2);                     // forward(&&) 2  process(&&) 2
  forward(std::move(c));          // forward(&&) 0  process(&&) 0
  forward(c);          // forward(&&) 0  process(&) 0

  return 0;
}

果然,一样实现了完美转发!!!那剩下的问题就是到底是在哪一步,判断出T到底是左值还是右值。

1.4 真相大白

我们进一步通过is_lvalue_reference和is_rvalue_reference可以判断i是左值引用还是右值引用

#include<iostream>
#include <cstring>
#include <vector>

using namespace std;
template<class T>
void process(T& i) {
  std::cout << "process(&) " << i << std::endl;
}

template<class T>
void process(T&& i) {
  std::cout << "process(&&) " << i << std::endl;
}

template<class T>
void forward(T&& i) {
  if (is_lvalue_reference<T>::value)
  {
      cout<<"T is_lvalue_reference"<<endl;
  }
  else if(is_rvalue_reference<T>::value)
  {
      cout<<"T is_rvalue_reference "<<endl;
  }
  else{
  	  cout<<"T is lefe value or right value"<<endl;
  }
  std::cout << "forward(&&) " << i << std::endl;
  process(i);
}


int main(int argc, char *argv[])
{
  int a = 0;
  int& c = a;
  forward(2);                     // forward(&&) 2  process(&&) 2
  forward(std::move(c));          // forward(&&) 0  process(&&) 0
  forward(c);          // forward(&&) 0  process(&) 0

  return 0;
}

事实果然如同印证的一样,i在调用std::forward就已经被判断出来了到底是左值引用还是右值引用;
在这里插入图片描述

有个评论总结的很好,如下:

std::forward必须配合T&&来使用。例如T&&接受左值int&时,T会被推断为int&,而T&&接受右值int&&时,T被推断为int。在std::forward中只是单纯的返回T&&。那么依据外层是左值时,T为int&,那么T&&即int& &&仍为int&,当外层函数参数为右值,T&&为int&&,这样就保证了传进来是左值则还是左值,是右值还是右值。

这个总结是针对于std::forward定义来描述的,按照这段描述,确实通过std::forward<T>中的static_cast<_Tp&&>(__t)能够实现上述的转换,但读者有没有发现,我在1.3中使用的是static_cast<T>而不是static_cast<_Tp&&>(__t),实际上这里还有一个隐藏的转换,就是发生在类型转换期间,我们看下这个例子:

#include<iostream>
#include <cstring>
#include <vector>
#include <type_traits>
using namespace std;

template<class T>
void process(T& i) {
  std::cout << "process(&) " << i << std::endl;
}

template<class T>
void process(T&& i) {
  std::cout << "process(&&) " << i << std::endl;
}

template<class T>
void forward(T&& i) {
  if (is_lvalue_reference<T>::value)
  {
      cout<<"T is_lvalue_reference"<<endl;
  }
  else if(is_rvalue_reference<T>::value)
  {
      cout<<"T is_rvalue_reference "<<endl;
  }
  else{
    cout<<"T is lefe value or right value"<<endl;
  }

  std::cout << "forward(&&) " << i << std::endl;
  process((T)i);
}


int main(int argc, char *argv[])
{
  int a = 0;
  forward(2);                     // forward(&&) 2  process(&&) 2
  forward(std::move(c));          // forward(&&) 0  process(&&) 0
  forward(c);          // forward(&&) 0  process(&) 0
  cout<<"++++++++++++++++"<<endl;
  process((int&)a);//process(&) 0
  process((int)a);//process(&&) 0
  return 0;
}

这里更加简单粗暴了,直接通过process((T)i);实现了完美转发,那是因为在类型转换时,也会发生左值到右值的转换,见代码的最后两行,对a进行(int&)强转得到的是左值,而对a
进行(int)强转得到的是右值,因此结合下面这个总结,是否彻底明白了完美转发的实现原理呢?在模板推到T的类型时,当传入的是右值int&&时,则(T)i被推导为(int)i,传入process的是右值,当传入的是左值int&时,则(T)i被推导为(int&)i,传入process的是左值,因此实现了完美转发。

std::forward必须配合T&&来使用。例如T&&接受左值int&时,T会被推断为int&,而T&&接受右值int&&时,T被推断为int。在std::forward中只是单纯的返回T&&。那么依据外层是左值时,T为int&,那么T&&即int& &&仍为int&,当外层函数参数为右值,T&&为int&&,这样就保证了传进来是左值则还是左值,是右值还是右值。

最后一个问题,希望有人能够解答下,既然完美转发直接通过(T)就能够实现,为什么在实际的std::forward(T)定义中是通过(T&&)来实现的。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值