就TM你叫std::forward啊?

之前介紹過std::move,今天我們就接着來說說std::forward。C++11引入了一個新特性:右值引用,這個特性可以避免不必要的拷貝從而提高性能。

std::forward

我們先看看std::forward是幹什麼的,然後說說爲什麼需要它。
根據前文,資源高效轉移的問題不是已經有std::move來解決了麼,爲什麼還需要另外一個std::forward來參和?注意“不必要”這個詞,既然有不必要,那麼就說明有時候是有必要的。
雖然std::movestd::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::forwardstd::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,掃描下方二維碼獲取更多精彩原創內容!
公衆號二維碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章