C++模板與泛型編程:模板實參推斷與引用,理解std::move,與轉發 (std::forward)

模板實參推斷與引用

​ 爲了理解如何從函數調用進行類型推斷,考慮下面例子:

template <typename T> void f(T &p);

對於模板參數是引用,需要注意兩點:編譯器會應用正常的引用綁定規則;對引用的 const 是底層的,不是頂層。

從左值引用函數參數推斷類型

​ 當一個函數參數是模板參數的一個左值引用時 (即 T&),我們只能傳遞給它一個左值。實參可以是 const 類型,也可以不是。如果實參是 const 的,則 T 被推斷爲 const 類型。

template <typename T> void f1(T&);	// 實參必須是一個左值
f1(i);		// i 是一個 int,則 T 是 int
f1(ci);		// ci 是 const int,模板參數 T 是 const int
f1(5);		// 錯誤,傳遞的實參必須是一個左值

​ 如果函數參數類型是 const T&,我們可以傳遞給它任何類型的實參——一個對象(const 或 非 const)、字面值、臨時對象。當函數參數本身是 const 時,T 類型推斷的結果不會是一個 const 類型。

template <typename T> void f2(const T&);	// 可以接受一個右值
// f2 中的參數是 const&;實參中的 const 是無關的
// 下面的調用中,**f2** 的函數參數都被推斷爲 const int&
f2(i);		// i 是 int,T 是 int
f2(ci);		// ci 是 const int,但 T 是 int
f2(5);		// T 是 int,const T& 可以綁定一個右值
從右值引用函數參數推斷類型

​ 當一個函數參數是一個右值引用時,我們可以傳給它一個右值。類型推斷過程類似左值引用函數參數的推斷過程。推斷出的 T 的類型是該右值實參的類型

template <typename T> void f3(T&&);
f3(42);			// T 是 int
引用摺疊和右值引用參數

​ 假定 i 是 int 對象,我們可能認爲 f3(i) 這樣是不合法的,畢竟 i 是一個左值,通常我們不能把一個左值綁定到右值引用。但是 C++在正常綁定規則之外定義了兩個例外規則,允許這種綁定。這兩個例外規則是 move 正確工作的基礎。

第一個例外規則影響右值引用參數的推斷如何進行。如上述情況,當我們調用 f3(i) 時,編譯器推斷 T 爲 int&,而非 int。即**,當我們將一個左值傳遞給函數的右值引用參數,且此右值引用指向模板類型參數時,編譯器推斷模板類型爲實參的左值引用類型**。!!注意:此右值引用指向模板類型參數 (!!!)時才滿足。

​ T 被推斷爲 int&,看起來好像 f3 的函數參數應該是一個類型 int& 的右值引用。一般情況下,我們不能定義一個引用的引用。但是通過類型別名或模板類型參數間接定義是可以的。

​ 在這種情況下,我們有第二個例外綁定規則:如果我們間接創建一個引用的引用,則這些引用形成了“摺疊”。在所有情況下,引用會摺疊成一個普通的左值引用類型。在新標準中,有一個例外:右值引用的右值引用爲摺疊成右值引用

​ 有以上兩個規則,意味着我們可以對一個左值調用 f3。

如果一個函數參數是指向模板參數類型的右值引用(T&&),我們可以傳遞給它任意類型的實參。如果將一個左值傳遞給這樣的參數,則函數參數被實例化爲一個普通的左值引用 (T&)。

編寫接受右值引用參數的模板函數

​ 模板參數可以推斷爲一個引用類型,這一特性對模板內的代碼可能有令人驚訝的影響:

template <typename T> void f3(T&& val) {
    T t = val;		// 拷貝還是綁定一個引用?
    t = fcn(t);		// 賦值只改變 t 還是既改變 t 又改變 val
    if(val == t) { /* */ }		// 若 T 是引用類型,則一直爲 true
}

當我們對一個右值調用 f3 的時候,例如字面常量 42,T 爲 int。在此情況下,局部變量 t 的類型爲 int,通過拷貝參數 val 的值被初始化。當我們對 t 賦值時,參數 val 不變。

​ 當我們對一個左值 i 調用 f3 時,T 爲 int&,因此 t 的初始化被綁定到 val,改變 t 將改變 val。if 判斷永遠都是 true。

同樣,右值引用 T&& 會與 const T& 形成重載。

理解 std::move

​ 我們知道,通過 move 可以獲得一個綁定到左值上的右值引用。由於 move 本質上可以接受任何類型的實參,因此我們可以知道它是一個函數模板。

std::move 是如何定義的

​ 標準庫是這樣定義 move 的:

template <typename T>
typename remove_reference<T>::type&& move(T &&t) {
    return static_cast<typename remove_reference<T>::type&&>(t);
}

我們可以發現,函數形參是 T&&,所以我們可以傳遞給 move 左值或者右值:

string s1("hi!"), s2;
s2 = std::move(string("bye!"));		// ok,從一個右值移動數據
s2 = std::move(s1);					// ok,但是在賦值後,s1 的值是不確定的
std::move 是如何工作的

​ 在第一個賦值中,傳遞給 move 的實參是 string 的構造函數的右值結果。如我們所見過的,當一個右值引用函數傳遞一個右值時,有實參推斷出的類型爲被引用類型。因此,在 std::move(string(“bye!”)) 中:

  • 推斷出的 T 的類型爲 string
  • 因此,remove_reference 用 string 進行實例化
  • remove_reference<string> 的 type 成員是 string
  • move 的返回類型是 string&&
  • move 的函數參數 t 的類型爲 string&&

這個調用實例化 move<string>,即函數:string&& move(string &&t),函數返回 static_cast<string&&>(t),實際並沒有發生類型轉換。因此,此調用結果返回它所接受的右值引用。

​ 考慮第二個賦值,傳遞給 move 的實參是一個左值:

  • 推斷出 T 的類型爲 string&
  • remove_reference 用 string& 實例化
  • remove_reference<string&> 的 type 成員是 string
  • move 仍然返回 string&&
  • move 的函數實參 t 實例化爲 string& &&,摺疊爲 string&。

因此,這個調用實例化 move<string&>,即:string&& move(string &t),通過類型轉換,得到 string&&。

從左值 static_cast 到一個右值引用是允許的

​ 雖然不能隱式地將一個左值類型轉換爲右值引用,但我們可以用 static_cast 顯式地將一個左值轉換爲一個右值引用。

轉發

​ 某些函數需要將其一個或多個實參連同類型不變地轉發給其他函數。在此情況下,我們需要保持被轉發實參的所有性質,包括實參類型是否是 const 的以及實參是左值還是右值。

​ 作爲一個例子,我們將編寫一個函數,它接受一個可調用表達式和兩個額外實參。我們的函數將調用給定的可調用對象,將兩個額外參數逆序傳遞給它。下面是我們的翻轉函數初步模樣:

// flip1 是一個不完整的實現,頂層 const 和引用丟失了
template <typename F,typename T1,typename T2>
void flip1(F f,T1 t1,T2 t2) {
    f(t2,t1);
}

這個函數一般情況下沒有問題。但當我們用它調用一個接受引用參數的函數時就會出現問題:

void f(int v1,int &v2) {
    cout << v1 << " " << ++ v2 << endl;
}

我們通過 flip1 調用 f,f 引用參數所做的改變不會影響實參:

f(42,i);			// i 的值會改變
flip1(f,j,42);		// j 的值不會改變

flip1(f, j, 42) 中 j 不會改變的原因是:j 被拷貝到 flip1 函數參數 t1,然後再調用的 f,所以實際上 f 改變的是 j 的拷貝,而沒有改變 j 本身。

定義能保持類型信息的函數參數

​ 爲了通過翻轉函數傳遞一個引用,我們需要重寫函數,使其參數能保持給定實參的“左值性”。更進一步,我們也希望保持參數的 const 屬性。

通過將一個函數參數定義爲一個指向模板類型參數的右值引用,我們可以保持其對應實參的所有類型信息。因爲 const 對應引用來說,它是底層的。模板類型參數是右值引用,我們能夠通過引用摺疊保持翻轉實參的左值/右值屬性。

template <typename F,typename T1,typename T2>
void flip2(F f,T1 &&t1,T2 &&t2) {
    f(t2,t1);
}

對於 flip2,當我們這樣調用時 flip2(f, j, 42),j 的值將會發生改變,因爲 j 是一個左值,我們將其綁定到右值引用時,T1 會被推斷爲 int&,引用摺疊後 t1 也就是 int&,所以 t1 會被綁定到 j。

​ flip2 對接受左值引用的函數工作沒有問題,但不能用於接受右值引用參數的函數,例如:

void g(int &&i,int &j) {
    cout << i << " " << j << endl;
}

如果我們試圖通過 flip2 調用 g,(無論傳遞給 flip2 的是左值還是右值):

flip2(g,i,42);

上述代碼會出現:不能從一個左值實例化 int&& 的錯誤 (!! 類型爲右值引用的變量,該變量本身仍是左值)。

在調用中使用 std::forward 保存類型信息

​ 我們可以使用一個名爲 forward 的新標準庫來傳遞 flip2 的參數,它能保持原始實參的類型。定義在頭文件 utility 中。forward 必須通過顯式模板實參來調用。

​ 通常情況下,我們使用 forward 傳遞那些定義爲模板類型參數的右值引用的函數參數。通過其返回類型上的引用摺疊,forward 可以保持給定實參的左值/右值屬性

​ 使用 forward,我們可以再次重寫翻轉函數:

template <typename F,typename T1,typename T2>
void flip(F f,T1 &&t1,T2&& t2) {
    f(std::forward<T2>(t2),std::forward<T1>(t1));
}

如果我們調用 flip(g, i, 42),i 將以 int& 類型傳遞給 g,42 將以 int&& 類型傳遞給 g。

與 std::move 相同,對 std::forward 不使用 using 聲明是一個好主意。

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