【C++11上手篇】贰、万能引用与完美转发

1,652 阅读4分钟

引子

前文说到,std::move 源码中参数_Tp&&看起来像是个右值引用,但是在使用时却可以接收左值。

  //std::move  
  template<typename _Tp>
    constexpr typename std::remove_reference<_Tp>::type&&
    move(_Tp&& __t) noexcept
    { return static_cast<typename std::remove_reference<_Tp>::type&&>(__t); }
  int main(){
      std::string a = "hello";
      std::string&& b = std::move(a); //参数a是左值
  }

实际上,这种情况下参数T&&是一个万能引用,或称做通用引用(Universal References)。 这块内容基本上只涉及到模板编程,但是为了能更好的啃下各种C++开源库,还是得了解下万能引用以及完美转发的概念。

万能引用与引用折叠

如果一个函数模板参数类型为T&&,其中T需要推导,那么T&&就是一个未定义的引用类型,称为万能引用,它既能绑定右值,又能绑定左值。 注意,只有当发生自动类型推断时(比如函数模板的类型自动推导,或者auto关键字),&&才是一个万能引用。 举个简单的例子,

template<typename T>
void func(T&& param){
    
}
int main(){
    //例子1
    func(1); //1是右值, param是右值引用
    int a = 2;
    func(a); //a是左值, param是左值引用

    //例子2
    std::string b = "hello"; 
    auto&& c = b; //auto&&绑定左值
    auto&& d = "world"; //auto&&绑定右值
}

例子中,T是一个模板,那么T就可能是int或int&或int&&,最后参数就可能变成(int&& && param)。 由于C++禁止reference to reference的情况,所以编译器会对L2L、L2R、R2L、R2R这四种引用做处理,折叠为单一引用,也就是引用折叠(Reference collapsing),具体就是:

  • T& &、T&& &、T& &&都折叠成T&
  • T&& &&折叠成T&& 这个比较好记,只要出现左值引用,都会最终折叠为左值引用。

完美转发

有了上面的概念之后,完美转发(Perfect Forwarding)这一块就很好理解了。 万能引用 + 引用折叠 + std::forward一起构成了完美转发的机制。

简单一点讲就是,std::forward会将输入的参数原封不动地传递到下一个函数中,如果是左值,传递到下一个函数还是左值,如果是右值,传递到下一个函数还是右值。

所谓perfect,指的就是不仅能准确地转发参数的值,还能保证其左右值属性不变。

为什么需要这个机制?

先看下面这段代码会输出什么?

template<typename T>
void func(T& param) {
    cout << "传入左值" << endl;
}
template<typename T>
void func(T&& param) {
    cout << "传入右值" << endl;
}

template<typename T>
void test(T&& t) { //参数t,万能引用
    func(t);
}

int main() {
    int a = 1;
    test(a);
    test(1);
}

输出是:

传入左值
传入左值

可以发现,无论传入左值右值,最终都调用了左值那个函数,和预期并不一致。

这是因为,无论调用test函数模板传递的是左值还是右值,对于函数内部的参数t来说,它有自己的名称,也可以获取地址,因此它永远都是左值。也就是说,传递给func函数的参数t一直是左值。(被声明的左值引用和右值引用本身就是一个左值,可以寻址)

上面这段话理解了之后,我们可以使用 std::forward 来改造下test函数,让它足够perfect。

template<typename T>
void test(T&& param) {
    func(std::forward<T>(param));
}

到这里,应该理解C++出现完美转发的动机了。

在C++很多场景中,是否实现参数的完美转发,直接决定了这个参数的传递过程使用的是移动语义还是拷贝语义。

最后,我们再瞄一眼std::forward的函数定义:

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

前文已经说过,typename std::remove_reference<_Tp>::type 的作用是去掉参数_Tp的引用,只保留类型。 我们根据_Tp的引用类型分别简化下模板代码:

  //情况1:接收左值,_Tp被推导为string&,那么_Tp&&就是string& &&,折叠为string&
  string& forward(string& __t) {
    return static_cast<string&>(__t); 
  }

  //情况2:接收右值,_Tp被推导为string&&,那么_Tp&&就是string&& &&,折叠为string&&
  string&& forward(string& __t) {
    return static_cast<string&&>(__t); 
  }

总结

到这里,我们已经讲清楚了移动语义和完美转发,右值引用的作用就是支持这些机制。 

说到底,这些新特性出现的意义是什么?C++11搞这么复杂,是不是多此一举? 

这几个特性绝对不是可有可无的东西,它们的最终目的就是为了减少额外的拷贝开销,提升代码性能。但是也确实提升了理解成本,毕竟这几个特性,最算精通其他编程语言,光看源码也无法完全理解。不过只要理解了设计动机,一切解释起来就简单了。 扫码_搜索联合传播样式-白色版.png