模板參數的「右值引用」是轉發引用

在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上撞撞牆才能學會一點東西。

相關文章
相關標籤/搜索