C++Primer_Chap16_模板和泛型程式設計_List02_模板實參推斷_筆記
從函式實參類確定模板實參的過程稱為模板實參推斷(template argument deduction)。
型別轉換和模板型別引數
如果一個函式形參的型別使用了模板型別引數,那麼它採用特殊的初始化規則。只有很有限的幾種型別轉換會自動應用於這些實參。編譯器通常不是對實參進行型別轉換,而是生成一個新的模板例項。能在呼叫中應用於函式模板的包括如下兩項:
- const轉換:可以將一個非const物件的引用(指標)傳遞給一個const引用和指標
- 陣列和函式指標轉換:如果函式形參不是引用型別,則可以對陣列或函式型別的實參應用正常的指標轉換。
其他型別轉換,如算術型別轉換、派生類向基類的轉換以及使用者定義的轉換都不能應用於函式模板。
template <typename T> T fobj(T, T); //實參被拷貝 template <typename T> T fref(const T&, const T&); //引用 string s1("a value"); const string s2("another value"); fobj(s1, s2); //呼叫fobj(string, string);const被忽略 fref(s1, s2); //呼叫fref(cosnt string&, const string&) //將s1轉換成const是允許的 int a[10], b[42]; fobj(a, b); //呼叫f(int*, int*) fref(a, b); //錯誤:陣列型別不匹配
使用相同模板引數型別的函式引數
一個模板型別引數可以用作多個函式形參的型別。由於只允許有限的幾種型別轉換,因此傳遞給這些形參的實參必須具有相同的型別。
long lng;
compare( lng, 1024); //錯誤:不能例項化compare(long, int)
如果希望允許對函式實參進行正常的型別轉換,我們可以將函式模板定義為兩個型別引數:
template <typename A, typename B> int filexibleCompare( const A& v1, const B& v2) { //…… }
正常型別轉換應用於普通函式實參
函式模板可以有用普通型別定義的引數,即,不涉及模板型別引數的型別。這種函式實參不進行特殊處理,它們正常轉換為對應形參的型別:
template <typename T> ostream &print( ostream &os, const T &obj)
{
return os << obj;
}
print(cout, 43); //例項化print(ostream&, int)
ofstream f("output");
print(f, 10); //使用print(ostream&, int):將f轉換成ostream&
函式模板顯式實參
指定顯式模板實參
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);
//T1顯式指定,T2和T3是從函式實參型別推斷而來的
auto val3 = sum<long long>(i, lng); //long long sum(int,long)
顯式模板實參按由左至右的順序與對應的模板引數匹配:第一個模板引數與第一個模板引數匹配,第二個實參與第二個引數匹配,依次類推。只有尾部(最右)引數的顯示模板實參才可以忽略:
//糟糕的設計:用於必須指定所有三個模板引數
template <typename T1, typename T2, typename T3>
T3 alternative_sum(T2, T1);
//錯誤:不能推斷前幾個模板引數
auto val3 = alternative_sum<long long>(i, lng);
正常型別轉換應用於顯示指定的實參
對於用普通型別定義的函式引數,允許進行正常的型別轉換:
long lng;
compare<long>(lng, 1024);
compare<int>(lng, 1024);
尾置返回型別與型別轉換
當我們希望使用者確定返回型別時,用顯式模板實參表示模板函式的返回型別是很有效的。但在其他情況下,要求顯式指定模板實參會給使用者增添額外的負擔且不會有什麼好處。
template <typename T>
??? &fcn(T beg, T end)
{
//……
return *beg;
}
vector<int> vi = {1, 2, 3, 4, 5};
Blob<string> ca = {"hi", "bye"};
auto &i = fcn(vi.begin(), bi.end());
auto &s = fcn(ca.begin(), ca.end());
在此例中,我們知道函式應該返回*beg,而且知道我們可以用decltype(*beg)來獲取此表示式的型別。但是在編譯器遇到函式的引數列表之前,beg都不存在。為了定義此函式,我們必須採用位置返回型別。
template <typename T>
auto fcn(T beg, T end)->decltype(*beg)
{
//……
return *beg;
}
進行型別轉換的標準庫模板類
有時我們無法直接獲取所需的型別。比如希望編寫一個返回元素值而不是引用的類似fcn的函式。對於傳遞的引數的型別,我們幾乎一無所知,唯一可以使用的操作符是迭代器操作,而所有迭代器操作都不會生成元素,只能生成元素的引用。為了獲取元素型別,我們可以使用標準庫的型別轉換(type transformation)模板。這些模板定義在標頭檔案中type_traits。這個標頭檔案中的類通常用於所謂的模板元程式設計。
#include <type_traits>
remove_reference<decltype(*beg)>::type
remove_reference::type脫去引用,剩下元素型別本身。我們必須在返回型別的什麼中使用typename來告知
templare <typename T>
auto fcn2(T beg, T end)->typename remove_reference<decltype(*beg)>::type
{
return *beg;
}
對Mod<T>,其中Mod為 | 若T為 | 則Mod<T>::type為 |
remove_reference | X&或X&& 否則 |
X T |
add_const | X&、const X或函式 否則 |
T const T |
add_lvalue_reference | X& X&& 否則 |
T X& T& |
add_rvalue_reference | X&或X&& 否則 |
T T&& |
remove_pointer | X* 否則 |
X T |
add_pinter | X&或X&& 否則 |
X* T* |
make_signed | unsigned X 否則 |
X T |
make_unsigned | 帶符號型別 否則 |
unsigned X T |
remove_extent | X[n] 否則 |
X T |
remove_all_extents | X[n1][n2]… 否則 |
X T |
函式指標和實參推斷
template <typename T> int compare(const T&, const T&);
//pf1指向例項int compare(const int&, const int&)
int (*pf1)(const int&, const int&) = compare;
//func的過載版本:每個版本接受一個不同的函式指標型別
void func(int(*)(const string&, const string&));
void func(int(*)(const int&, const int&));
//錯誤:二義性
func(compare);
//正確:顯式指出例項化版本
fun(compare<int>);
當引數是一個函式模板例項的地址時,程式上下文必須滿足:對每個模板引數,能唯一確定其型別或值
模板實參推斷和引用
template <typename T> void f(T &p);
函式引數p是一個模板型別引數T的引用,非常重要的是記住兩點:編譯器會應用正常的引用繫結規則;const是底層的,不是頂層的。
從左值引用函式引數推斷型別
當一個函式引數是模板型別引數的一個普通(左值)引用時(T&),只能傳遞給它一個左值。實參如果是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)、一個臨時物件或是一個字面值常量。
template <typename T> void f2(const T&);
f2(i); //i:int; T:int
f2(ci); //ci:const int; T: int
f2(5); //正確
引用摺疊和右值引用引數
template <typename T> void f3(T&&);
通常不能將一個右值引用繫結到一個左值上。但C++語言在正常繫結規則之外定義了兩個例外規則,允許這種繫結。這兩個例外規則是move這種標準庫設施正確工作的基礎:
- 第一個例外規則影響右值引用引數的推斷如何進行。當將一個左值傳遞給函式的右值引用引數,且此右值引用指向模板型別引數(T&&),編譯器推斷模板型別引數為實參的左值引用型別。通常,不能直接定義一個引用的引用,但通過類型別名或通過模板型別引數間接定義是可以的。
- 如果間接建立了一個引用的引用,則這些引用形成了“摺疊”。在所有情況下(除了一個例外),引用會摺疊成一個普通的左值引用型別。只在一種特殊情況下引用會摺疊成右值引用:右值引用的右值引用。即,給定一個型別X:
- X& &,X& &&和X&& &都摺疊成型別X&
- X&& &&摺疊成X&&
引用摺疊值能應用於間接建立的引用的引用,如類型別名或模板引數。
f3(i); //實參是一個左值;模板引數T是int&
f3(ci); //實參是一個左值;模板引數T是const int&
//用於演示的無效程式碼
void f3<int &>(int& &&); //T是int&,函式引數是int& &&。摺疊為int&
如果一個函式引數是指向模板引數型別的右值引用(T&&),則可以傳遞給它任一型別(左值或右值)的實參。如果將一個左值傳遞給這樣的引數,則函式引數被例項化為一個普通的左值引用。
template <typename T> void f3(T&& val)
{
T t = val; //拷貝 OR 繫結一個引用
t = fcn(t); //賦值只改變t OR 又改變t又改變val
if(val == t) //若T為引用型別,一直為true
{
/* */
}
}
當我們f3(42)時,T為int,區域性變數t是int,通過拷貝引數val的值初始化。對t賦值,引數val保持不變
當我們f3(i)時,T為int&,定義並初始化區域性變數t時,型別為int&。
在實際中,右值引用通常用於兩種情況:模板轉發其實參或模板被過載。過載通常如下方式:
template <typename T> void f(T&&); //繫結到非const右值
template <typename T> void f(const T&); //左值和const右值
理解std::move
標準庫如下定義move
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type&&>(t);
}
move的函式引數T&&是一個指向模板型別引數的右值引用。通過引用摺疊,此引數可以與任何型別的實參匹配。
string s1("hi"), s2;
s2 = std::move(string("bye")); //正確,從一個右值移動資料
s2 = std::move(s1); //正常:但在賦值後,s1的值是不確定的
在std::move(string("bye"))中:
- 推斷出T的型別是string
- 因此,remove_reference用string例項化
- remove_reference<string>::type是string
- move返回型別是static_cast<string&&>(t),即string&&
- move的函式引數t的型別是string&&
- 所以這個呼叫例項化move<string>,即函式 string&& move(string &&t)
在std::move(s1)中:
- T的型別是string的引用
- 因此,remove_reference用string&例項化
- remove_reference<string&>::type是string
- move返回型別是static_cast<string&&>(t),即string&&
- move的函式引數t例項化為string& &&,這得為string&
- 所以這個呼叫例項化move<string&>,即函式string&& move(string &t)
從一個左值static_cast到一個右值引用是允許的。通常情況,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, j, 42); //通過flip1呼叫f不會改變j
這個flip1函式在呼叫一個接受引用引數的函式時會出現問題。
定義能保持型別資訊的函式引數
如果一個函式引數是指向模板型別引數的右值引用,它對應的實參的const屬性和左/右值屬性將得到保持。
template<typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
f(t2, t1);
}
void g(int &&v1, int &v2)
{
cout << v1 << " " << ++v2 << endl;
}
flip( g, i, 42); //錯誤:不能從一個左值例項化int&&
這個版本的flip2解決了一半問題,但不能用於接受右值引用引數的函式。在flip2中i是int&,到g中v1應該是int&&,型別轉換出錯。
在呼叫中使用std::forward保持型別資訊
我們可以使用一個名為forward的新標準庫來傳遞引數,它能保持原始實參的型別。必須通過顯式模板實參來呼叫,返回該顯式實參型別的右值引用:
#include <utility>
template <typename Type> intermediary(Type &&arg)
{
finalFcn(std::forward<Type>(arg));
}
template<typename F, typename T1, typename T2>
void flip2(F f, T1 &&t1, T2 &&t2)
{
f(std::forward<T2>(t2), std::forward<T1>(t1));
}
與std::move相同,對std::forward不適用using宣告是一個好主意