在C++11中, 不再只有邏輯與的含義,還可能是右值引用: 但也不盡然, 還可能是轉發引用: “轉發引用”(forwarding reference)舊稱“通用引用”(universal reference),它的“通用”之處在於你可以拿一個左值綁定給轉發引用,但不能給右值引用: 一個函數的參數要想 ...
在C++11中,&&
不再只有邏輯與的含義,還可能是右值引用:
void f(int&& i);
但也不盡然,&&
還可能是轉發引用:
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&&
,沒有const
或volatile
; -
T
必須是該函數的模板參數。
換言之,以下函數的參數都不是轉發引用:
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&&
:
std::vector<int> vec;
for (auto&& i : vec)
{
// ...
}
有一個例外,當auto&&
右邊是初始化列表,如auto&& l = {1, 2, 3};
時,該變數為std::initializer_list<int>&&
類型。
轉發引用,是用來轉發的。只有當你的意圖是轉發參數時,才寫轉發引用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);
,T
為int&
,調用g(int&)
; -
const int j = 2; g(j);
,T
為const int&
,調用g(const int&)
; -
int k = 3; g(std::move(k));
或g(4);
,T
為int
(不是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&
時,匹配第一個重載,_Tp
即T
為int&
,返回類型_Tp&&
為int&
(引用摺疊:& &
、& &&
、&& &
都摺疊為&
,只有&& &&
摺疊為&&
); -
const int&
同理; -
當轉發引用綁定右值
int&&
時,匹配第二個重載,_Tp
為int
,返回類型為int&&
; -
const int&&
同理。
綜上,std::forward
能完美轉發。
程式員總是要在Stack Overflow上撞撞牆才能學會一點東西。