C++完美转发

一、什么是完美转发

提到完美转发,就有必要说一下,什么是转发,什么样的转发才称得上是完美转发。

在C++中,转发指的就是函数之间的参数传递(例如函数f1接收了一个参数a,而后又将此参数a 传递给了其函数体内调用的另一个函数f2)

而完美转发指的就是在函数之间传递参数的过程中,参数在传递后的属性保持不变(如左值仍是左值,右值仍是右值,const修饰也会保留)。

二、常规转发存在的问题

对于普通的转发,参数在函数间传递时属性可能会发生改变,我们看一个例子。

#include <iostream>
#include <utility>

void an_orther_fun(int a, int& b)
{
    std::cout << "in an_orther_fun(): a = " << a << ", b = " << ++b << std::endl;
}

void transmit(int a, int b)
{
    an_orther_fun(a, b);
}

int main()
{
    int a = 2, b = 3;

    std::cout << " before transmit(): a = " << a << ", b = " << b << std::endl;
    
    transmit(a, b);
    
    std::cout << "  after transmit(): a = " << a << ", b = " << b << std::endl;
    
    return 0;
}

其输出为:

before transmit(): a = 2, b = 3
in an_orther_fun(): a = 2, b = 4
after transmit(): a = 2, b = 3

注意,函数an_orther_fun()的第二个参数类型是引用int&,并且我们在函数中给该引用的值加上了1(++b那里),也就是我们预期应当会修改其第二个参数的值加1。但根据输出,虽然我们在an_orther_fun()打印出了加1后的b的值。但在外层,我们执行tansmit()后,b的值并没有加 1。

这里的原因其实很显而易见的,就是我们b在从transmit()传递到an_other_fun()的时候,其属性已经改变了:transmit()中的b是外层b的一个副本,而不是引用。

这就是常规引用可能带来的转发问题。

你可能会说,我们将transmit()的第二个参数类型也改为引用int&不就可以解决了吗?

void transmit(int a, int& b) { ... }

在我们这个示例中,这样的修改确实是可以达成我们示例中的目的的。

但,这只是示例,实际还有更多的可能性。例如,如果上例中的b处是一个右值引用,怎么办?我们看下面的代码:

#include <iostream>
#include <utility>

void an_orther_fun(int a, int&& b)
{
    std::cout << "in an_orther_fun(): a = " << a << ", b = " << b << std::endl;
}

void transmit(int a, int&& b)
{
    an_orther_fun(a, b);  // error: cannot bind rvalue reference of type ‘int&&’ to lvalue of type ‘int’
}

int main()
{
    int a = 2;

    std::cout << " before transmit(): a = " << a << ", b = " << b << std::endl;
    transmit(a, 3);   
    std::cout << "  after transmit(): a = " << a << ", b = " << b << std::endl;
    
    return 0;
}

这里感觉像是c++自己埋的坑:想使用生命周期,又没设计好,搞得语法有点模糊,c++里面的编译期模板函数有作用,通过返回右值引用的函数可以将左值转化为右值。

void transmit(int a, int &&b) {
  cout << "b's     type is " << type_id_with_cvr<decltype(b)>().pretty_name()
       << endl;
}

int main() {
  int a = 2;
  int &&b = 3;
  // 所以这里不需要一定使用move
  // static_cast<decltype(b)>(b)
  transmit(a, std::forward<decltype(b)>(b));
  return 0;
}

三、使用完美转发

完美转发需要使用到标准库中的std::forward<>()函数,其定义在头文件<utility>,其必须通过显式模板实参来调用。

其有两个重载,一个接收左值引用类型参数,另一个接收右值引用类型参数,定义如下:

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);
  }

可以看到std::constexpr<Type>的返回类型是_Tp&&。需要注意的是这里会触发引用折叠,std::forward<Type>()可以保持给定实参的左/右引用的属性

修改后transmit()像下面这样

template <typename F, typename T1, typename T2>
void transmit(F f, T1&& t1, T2&& t2)
{
    f(std::forward<T1>(t1), std::forward<T2>(t2));
}

3.1 传递左值分析

我们使用模板修改上面的代码,先以在函数间传递左值为例:

这里 an_other_fun() 函数接受的第二个参数是引用,并且会修改该值:

void an_orther_fun(int a, int& b)
{
    std::cout << "in an_orther_fun(): a = " << a << ", b = " << ++b << std::endl;
}

整体代码如下:

void an_other_fun(int a, int &b) {
  std::cout << "in an_orther_fun(): a = " << a << ", b = " << ++b << std::endl;
}

template <typename F, typename T1, typename T2>
void transmit(F f, T1 &&t1, T2 &&t2) {  // T1=>int&, T2=>int&, 因为`int a`是传值,所以这里不会产生什么影响
  f(std::forward<T1>(t1), std::forward<T2>(t2));
}

int main() {
  int a = 2, b = 3;

  std::cout << " before transmit(): a = " << a << ", b = " << b << std::endl;

  transmit(an_other_fun, a, b);

  std::cout << "  after transmit(): a = " << a << ", b = " << b << std::endl;

  return 0;
}

输出如下:

 before transmit(): a = 2, b = 3
in an_other_fun(): a = 2, b = 4
  after transmit(): a = 2, b = 4

可以看到外层的b确实发生了值修改,接下来对于transmit函数进行分析:

template <typename F, typename T1, typename T2>
void transmit(F f, T1 &&t1, T2 &&t2) {  
  // T1=>int&, T2=>int&, 因为`int a`是传值,所以这里不会产生什么影响
  f(std::forward<T1>(t1), std::forward<T2>(t2));
}

T2 &&t2为万能引用,b的表达式类型为左值,所以type(T2)==int&,根据引用折叠原理type(t2)==int&。函数std::forward<T2>(t2)的返回值的类型同样为int&,表达式类型为左值。

3.2 传递右值分析

...
void an_orther_fun(int a, int&& b)  // 注意这里第二个参数是右值引用类型 int&&
{
    ...
}

...

template <typename F, typename T1, typename T2>
void transmit(F f, T1&& t1, T2&& t2)
{
    f(std::forward<T1>(t1), std::forward<T2>(t2));
}

...
transmit(an_orther_fun, a, 3);
...

对于函数void transmit(F f, T1&& t1, T2&& t2)而言,T2会被推导为int类型,从而可以推导出std::forward<T2>(t2)的返回值为int&&

参考

  1. c++完美转发
  2. 生命周期
  3. 表达式类型
  • 9
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值