之前介紹過std::move
,今天我們就接着來說說std::forward
。C++11引入了一個新特性:右值引用,這個特性可以避免不必要的拷貝從而提高性能。
std::forward
我們先看看std::forward
是幹什麼的,然後說說爲什麼需要它。
根據前文,資源高效轉移的問題不是已經有std::move
來解決了麼,爲什麼還需要另外一個std::forward
來參和?注意“不必要”這個詞,既然有不必要,那麼就說明有時候是有必要的。
雖然std::move
和std::forward
都和右值引用有關,但是側重點不同。std::move
用在需要只右值引用的地方;而std::forward
用在一個需要統一引用(universal references)的地方,這個通用引用是什麼?我更喜歡叫它薛定諤的引用,因爲它到底是左值引用還是右值引用是不確定的,如果你給他傳遞左值它就是左值引用,如果給它傳個右值它就是右值引用。形如
template<typename T>
T f(T&& param) {}
這種T&&
就是通用引用。
假設我們有下面這麼一個類,Foo
,在使用的過程中會出現以下兩種初始化方式:
class Foo {
public:
std::string member_;
Foo(const std::string& member): member{member} {}
}
// Two use cases
// Case#1
std::string bar = "bar";
Foo foo(bar);
// Case#2
std::string bar = "bar";
Foo foo("foo" + bar);
這兩種方式有什麼不同呢?第一種使用場景中,我們已經有了一個字符串"bar"
和引用bar
綁定在了一起,我們希望用它來初始化Foo
,但是這個bar
我們後續還需要使用,所以我們希望它拷貝一份給Foo
;第二種情況中,"foo" + bar
這個表達式生成了一個臨時字符串"foobar"
,由於它是臨時的,外部是沒有任何引用和他綁定的,很快就會被銷燬,因此我們希望能將它的內存資源直接轉移給Foo
而不是拷貝一份。鑑於存在上述兩種使用場景,常規情況下我們需要分別定義兩個構造函數:
class Foo
{
public:
std::string member;
// Copy member.
Foo(const std::string& member): member{member} {}
// Move member.
Foo(std::string&& member): member{std::move(member)} {}
};
但是我們懶,不想寫那麼多構造函數,有沒有辦法實現?有。我們使用std::forward
:
class Foo
{
public:
std::string member;
template<typename T>
Foo(T&& member): member{std::forward<T>(member)} {}
};
如果上面不夠清晰的話,我們來看看下面這個例子:
#include <iostream>
#include <string>
#include <utility>
void foo(std::string& param) {
std::cout << "std::string& version" << std::endl;
}
void foo(std::string&& param) {
std::cout << "std::string&& version" << std::endl;
}
template<typename T>
void wrapper(T&& param) {
// foo(param);
foo(std::forward<T>(param));
}
int main() {
std::string foo("foo");
wrapper(foo);
wrapper(foo + "bar");
}
再上面的例子中,如果在wrapper
中沒有使用std::forward
,也就如果使用註釋掉的那個方法調用foo
函數,得到的結果將是這樣子:
std::string& version
std::string& version
而如果使用目前的方式調用foo
,結果將是:
std::string& version
std::string&& version
std::forward
到底做了什麼?
它主要作用如下:根據模板參數T
,將模板函數的形參param
變成在右值傳遞給函數foo
或者將param
保留爲左值傳遞給函數foo
。什麼意思呢?就是如果傳遞個形參param
的值是左值,例如上面例子中的foo
,那麼std::forward
返回的是一個左值;果傳遞個形參param
的值是右值,例如上面例子中的表達式foo + "bar"
得到的是一個右值,那麼std::forward
返回的是一個右值。因爲根據C++語義,在函數wrapper
的內部,param
是一個左值引用。
總的一句話就是std::forward
能夠保留傳給形參param
的實參的全部信息。wrapper(foo);
中參數foo
是左值,那麼wrapper
傳給函數foo
的就是左值;wrapper(foo + "bar");
中參數foo + "bar"
是右值,那麼wrapper
傳給函數foo
的就是右值。
但是,std::forward
是怎麼知道一個形參的原本類型的呢?這裏又引出兩個知識點:模板參數類型推導( template argument deduction)和引用則疊(Reference collapsing)
引用則疊和模板參數類型推導
關於引用則疊和模板參數推斷,可以說上一天,所以這裏步打算展開,僅僅簡單介紹下什麼是引用則疊。假設有下面這種情況:
// T denotes the int& type
typedef int& T;
// TR is an lvalue reference to T
typedef T& TR;
// The declared type of var is TR
TR var;
變量var
的類型是TR
,而TR
是類型T
的移用,T
右是int
類型的一個引用,這樣一串下來,var
的真實類型是什麼呢?
引用的引用,不管是左值引用還是右值引用,在C++11之前是非法的,但是上面例子中的這種情況又是很可能出現的。爲了解決這一問題,C++11定義了一套規則去處理引用的引用是什麼的問題,這就是移用則疊。
引用則疊主要右以下四條規則:
T | TR | Type of var |
---|---|---|
A& | T& | A& |
A& | T&& | A& |
A&& | T& | A& |
A&& | T&& | A&& |
套用到上面的小例子,var
的類型就是一個int&
。那這個到底和std::forward
有什麼關係呢?這裏留下一個坑,關於模板參數類型,以後有機會再說啦。
總結
std::forward
與std::move
一樣,都與C++11引入的新特性右值引用相關。但是,與std::move
不同的是,std::forward
可以將參數保留它的類型信息,原樣轉發給下一個被調用的函數。實現這一動作的原理是模板參數推導和引用則疊。
References
[1] ppreference.com
[2] Perfect Forwarding in C++11
[3] Reference collapsing (C++11)
[4] Advantages of using forward
本文首發於個人微信公衆號TensorBoy。如果你覺得內容還不錯,歡迎分享並關注我的微信公衆號TensorBoy,掃描下方二維碼獲取更多精彩原創內容!