模板参数的“右值引用”是转发引用

在C++11中,&&再也不只有逻辑与的含义,还多是右值引用:ios

void f(int&& i);

但也不尽然,&&还多是转发引用:c++

template<typename T>
void g(T&& obj);

“转发引用”(forwarding reference)旧称“通用引用”(universal reference),它的“通用”之处在于你能够拿一个左值绑定给转发引用,但不能给右值引用:程序员

void f(int&& i) { }

template<typename T>
void g(T&& obj) { }

int main()
{
    int n = 2;
    f(1);
//  f(n); // error
    g(1);
    g(n);
}

一个函数的参数要想成为转发引用,必须知足:函数

  • 参数类型为T&&,没有constvolatilecode

  • T必须是该函数的模板参数。rem

换言之,如下函数的参数都不是转发引用:get

template<typename T>
void f(const T&&);
template<typename T>
void g(typename std::remove_reference<T>&&);
template<typename T>
class A
{
    template<typename U>
    void h(T&&, const U&);
};

另外一种状况是auto&&变量也能够成为转发引用:编译器

auto&& vec = foo();

因此写范围for循环的最好方法是用auto&&it

std::vector<int> vec;
for (auto&& i : vec)
{
    // ...
}

有一个例外,当auto&&右边是初始化列表,如auto&& l = {1, 2, 3};时,该变量为std::initializer_list<int>&&类型。io

转发引用,是用来转发的。只有当你的意图是转发参数时,才写转发引用T&&,不然最好把const T&T&&写成重载(若是须要的话还能够写T&,还有不经常使用的const T&&;其中T是具体类型而非模板参数)。

转发一个转发引用须要用std::forward,定义在<utility>中:

#include <utility>

template<typename... Args>
void f(Args&&... args) { }

template<typename T>
void g(T&& obj)
{
    f(std::forward<T>(obj));
}

template<typename... Args>
void h(Args&&... args)
{
    f(std::forward<Args>(args)...);
}

调用g有几种可能的参数:

  • int i = 1; g(i);Tint&,调用g(int&)

  • const int j = 2; g(j);Tconst int&,调用g(const int&)

  • int k = 3; g(std::move(k));g(4);Tint(不是int&&哦!),调用g(int&&)

你也许会疑惑,为何std::move不须要<T>std::forward须要呢?这得从std::forward的签名提及:

template<typename T>
constexpr T&& forward(std::remove_reference_t<T>&) noexcept;
template<typename T>
constexpr T&& forward(std::remove_reference_t<T>&&) noexcept;

调用std::forward时,编译器没法根据std::remove_reference_t<T>反推出T,从而实例化函数模板,所以<T>须要手动指明。

可是这并无从根本上回答问题,或者能够进一步引出新的问题——为何std::forward的参数不定义成T&&呢?

缘由很简单,T&&会把T&const T&T&&const T&&(以及对应的volatile)都吃掉,有了T&&之后,再写T&也没用。

且慢,T&&参数在传入函数是会匹配到T&&吗?

#include <iostream>
#include <utility>

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

void foo(const int&)
{
    std::cout << "const int&" << std::endl;
}

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

void bar(int&& i)
{
    foo(i);
}

int main()
{
    int i;
    bar(std::move(i));
}

不会!程序输出int&。在函数bar中,i是一个左值,其类型为int的右值引用。更直接一点,它有名字,因此它是左值。

所以,若是std::forward没有手动指定的模板参数,它将不能区分T&T&&——那将是“糟糕转发”,而不是“完美转发”了。

最后分析一下std::forward的实现,如下代码来自libstdc++:

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);
  }
  • 当转发引用T&& obj绑定左值int&时,匹配第一个重载,_TpTint&,返回类型_Tp&&int&(引用折叠:& && &&&& &都折叠为&,只有&& &&折叠为&&);

  • const int&同理;

  • 当转发引用绑定右值int&&时,匹配第二个重载,_Tpint,返回类型为int&&

  • const int&&同理。

综上,std::forward能完美转发。

程序员老是要在Stack Overflow上撞撞墙才能学会一点东西。

相关文章
相关标签/搜索