在泛型編程中,常常需要將參數原封不動的轉發給另外一個函數,比如std::make_shared<T>(Args&&... args) 就需要將參數完美轉發到T對應的構造函數中。為了實現完美轉發,
std增加了forward工具函數, 完美轉發主要目的一般都是為了避免拷貝,同時調用正確的函數版本。
為了理解完美轉發,首先要理解左值與右值。
一、 為了更深刻的理解左值與右值,我們先來復習一下decltype表達式
decltype(expression) 可以獲取表達式的類型:
1,expression 是一個函數時,decltype 返回函數的返回值類型,沒有任何問題
2,expression 是單個變量時,decltype 返回變量的類型,依舊沒有任何問題
2,當expression 是某個表達式時,decltype 返回表達式的值的類型,這里會有一點讓人迷惑的地方,看下面的代碼:
void f(int&& a ) { int i = 0; int &ri = i; cout << boolalpha; cout << is_same<decltype(ri), int&>::value << endl; //true cout << is_same <int, decltype(i)>::value << endl; // true cout << is_same <double, decltype(3 + 4.0)>::value << endl; // true cout << is_same <int&, decltype((i))>::value << endl; //true cout << is_same <int, decltype(3 + 4)>::value << endl; //true cout << is_same<int&,decltype((a))>::value << endl; //true cout << is_same<int&&, decltype((a))>::value << endl; //false }
首先, ri 是一個類型為 int& 的單個變量,decltype(ri) 得到 int& 沒有任何問題, 同理, decltype (i) 也理所當然的得到 int, 因為 i 是 int 型變量並且在定義時分配了sizeof(int)的棧空間的。
接下來3 + 4.0 是一個表達式, 這個表達式返回一個右值, 該右值的類型為 double, 所以 decltype(3 + 4.0) 是 double
(i) 是一個表達式, 對於只有單個變量的表達式,c++編譯器的處理是返回一個綁定到該變量的左值引用,所以decltype((i)) 返回 int&
下面重點來了,decltype((a)) 返回的類型依舊是 int&, 理由同上,對於(x) 這樣的表達式,返回值是 x 的左值引用, 即使 x 的類型是 右值引用,這也就是說,我們可以將左值引用綁定到右值引用上,但是右值引用必須綁定到右值或者匿名的右值引用上 (如std::move()的返回值就是一個匿名右值引用)。
在進行右值綁定的時候,與 decltype 只會把形如 (a) 的單個變量看成表達式不同, 右值綁定時會把所有的 單個變量 都看成變量表達式, 然而單個變量的表達式返回的是一個左值引用, 然而右值引用無法綁定到左值引用, 最終造成的結果是我們無法把一個右值引用直接綁定到另一個右值引用上, 看下面的代碼:
void f(int&& a ) { int i = 0; int &&ri = a; //編譯錯誤,等價於 int &&ri = (a) 無法將一個右值引用綁定到另一個右值引用上 cout << boolalpha; cout << is_same<int&&, decltype(a)>::value << endl; //true }
其實也很好理解,單個右值引用表達式屬於 具名的右值引用, 這個引用本身也是得等到離開作用於才消失的, 也就不是短暫的,所以自然也就無法將一個右值引用綁定到另一個具名的右值引用上了, 所以非得這么搞, 只能綁定到匿名的右值引用, 如:
int &&rri = std::move(a); // 正確, std::move 返回一個匿名右值引用 cout << is_same<int&&, decltype(rri)>::value <<endl; // true cout << is_same<int&&, decltype((std::move(rri)))>::value << endl; //true
如果你覺得這種錯誤毫無意義的話,那么看看下面這段代碼:
void g(int &&) { cout << "right reference" << endl; } void g(const int&) { cout << "left reference" << endl; } void f(int&& a ) { /* do something*/ g(a); /* do something*/ }
對於 void g(int && arg), 試圖把右值引用 arg 綁定到另一個右值引用 a 上, 然而在綁定時 a 被當成單個變量表達式從而得到的類型是 int&, 從而 f 調用的是 void g (int & arg); 在進行右值綁定時,凡是有名字的都必然是左值/左值引用, 所以為了使用 g (int &&) 我們必須顯式的造出一個匿名的右值引用來:
void f(int&& a ) { /* do something*/
g(a); // g ( (std::move(a)) ) 同樣會調用右值引用版本, 因為 ( std::move(a) ) 不屬於 單個變量表達式, 也返回了一個匿名右值引用
/* do something*/ }
從上面的例子可以看出來, 要實現完美轉發需要這樣:
void f (const int& x) { g (x); // g 接受 const int& }
或者這樣 :
void f (const int& x) { g (const_cast<int&>(x)); // g 接受int& }
或者
void f (int&& x) { g (std::move(x)); //g 接受右值引用 }
那如果在模板中應該怎么做呢?
現在說明使用std::forward實現完美轉發的原理:
1,forward<T>(x) 能夠將x的類型變成 T&&, 即 forward<T> 返回類型是 T&&,
2,凡是使用forward來實現完美轉發的,接受參數時都應該寫成 void f(T&& x) { g(std::forward<T>(x)); }這樣, 即 f 接受的參數類型必須是T&&, g 接受的參數應該是std::foward<T>(x)
那么就以
template <typename T> void f (T&& arg) { g ( std::forward<T>(arg) ); }
為例來說明完美轉發:
1, 當 f 收到一個類型為 const int 的左值/左值引用 時, 由於引用折疊,T的類型為 const int&, f 被實例化成
void (const int &arg) { g (std::forward<const int&>(arg)); }
此時 g 收到的類型為 const int &&& ---> const int&,一個常量左值引。
2, 當 f 收到一個類型為 int 的左值/左值引用, 同樣的,g 收到的類型為 int&
3, 當 f 收到一個 int 類型的右值時, T 被推斷成 int, 從而 std::forward<int> 返回的類型為 int&&, 從而 g 正確的收到了右值參數
4, 再次強調為什么 f 一定要寫成 void f (T&&), 首先最最基本的要求就是, 傳遞給 g 的參數不是 f 拷貝后的參數, 而是 f 收到的參數,所以一定要是引用,絕對不能是拷貝,至於寫成右值引用的形式, 是為了接受右值。不管 f 接受到的是左值還是左值引用,最后 T 都會變成 左值引用並且不影響 g 的接受, 不管接受的是右值還是匿名右值引用(上面提到具名右值引用,會在綁定時當成表達式而變成左值引用,int &&a; 是具名右值引用, std::move(a) 則得到一個匿名右值引用, 4 + 3 這樣的得到的是右值), 最終都會被std::forward變成匿名右值引用被 g 正確接受
所以最后來體會一次完美轉發吧:
template <typename T> void f (T&&) { cout << boolalpha; cout << "is int ? : " << is_same<int, T>::value << endl; cout << "is int& ? : " << is_same<int&, T>::value << endl; cout << "is int&& ? : " << is_same<int&&, T>::value << endl; cout << endl; } int main() { int i = 0; int &ri = i; int &&rri = 4; f(i); //左值 f(ri); // 左值引用 f(0); //右值 f(std::move(i)); // 匿名的右值引用, 正確識別 f(rri); //實際上 f 收到的是左值引用, 右值引用只能綁定到 右值 上,而不能綁定到另一個右值引用上, 左值引用卻可以綁定到另一個右值引用或者另一個左值引用或者左值上 return 0; }