C++模板與泛型編程:模板實參推斷 (類型轉換與模板類型實參、函數模板顯式實參、尾置返回類型與類型轉換、函數指針和實參推斷)

模板實參推斷

​ 我們知道,對於函數模板,編譯器利用調用中的函數實參來確定其模板參數。從函數實參來確定模板實參的過程被稱爲模板實參推斷。在模板實參推斷過程中,編譯器使用函數調用中的實參類型來尋找模板實參,用這些模板實參生成的函數版本與給定的函數調用最爲匹配。

類型轉換與模板類型實參

​ 在一次調用中傳遞給函數模板的實參被用來初始化函數的形參。當函數參數是模板類型參數時,它會採用特殊的初始化規則。只有很有限的幾種類型轉換會自動應用於這些實參。編譯器通常不是對實參進行類型轉換,而是生成一個新的模板實例

​ 同樣,頂層 const 無論在形參還是實參,都會被忽略。在其他類型轉換中,能在調用中應用於函數模板的包括以下兩項:

  • 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 val");
fobj(s1,s2);		// 調用 fobj(string,string),實參頂層 const 被忽略
fref(s1,s2);		// 調用 fref(const string&,const string&),形參頂層 const 被忽略

int a[10],b[52];
fobj(a,b);			// 調用 fobj(int*,int*);
fref(a,b);			// 錯誤

在 fobj 中,數組會自動轉換爲指針。但是在 fref 中,因爲模板形參是引用,所以最後一個調用是錯誤的。

使用相同模板參數類型的函數形參

一個模板類型參數可以用作多個函數形參的類型。但由於只允許兩種類型的類型轉換,因此傳遞給這些形參的實參類型必須相同。如果推斷出的類型不匹配,則調用就是錯誤的。

​ 如我們有:

template <typename T> int compare(const T&,const T&);

當我們的調用方式爲:

long lng;
compare(lng,1024);

這是錯誤的,因爲 lng 是 long 類型,是 1024 是 int 類型。該調用無法實例化 compare 函數。

如果想要允許對函數實參的類型轉換,我們可以將函數模板定義爲兩個類型參數

template <typename T,typename E> int compare(const T&,const E&);

這樣,便可以提供不同類型的實參。如上述調用,便合法。

正常類型轉換應用於普通函數實參

​ 函數模板可以有普通類型定義的參數,即不涉及模板類型參數的類型。這種函數實參不進行特殊處理;它們正常轉換爲對應形參的類型。如下函數模板:

template <typename T> ostream &print(ostream &os,const T &obj) {
    return os << obj;
}

我們可以看見,該函數模板的第一個參數是 ostream&,所以當我們調用此函數時,傳遞給它的實參會進行正常的類型轉換:

print(cout, 42);		// 實例化 print(ostream&, int);
ofstream f("output");
print(f,10);			// 使用 print(ostream&, int); f 轉換爲 ostream&

函數模板顯式實參

​ 在某些情況下,編譯器無法推斷出模板實參的類型。在其他一些情況下,我們希望允許用戶控制模板實例化。當函數返回類型與參數列表中任何類型都不相同時,這兩種情況最常出現。

指定顯式模板實參

​ 作爲一個允許用戶指定使用類型的例子,我們將定義一個名爲 sum 的函數模板,它接受兩個不同類型的參數。我們希望允許用戶指定結果的類型。這樣,用戶就可以選擇合適的精度。

​ 我們可以定義表示返回類型的第三個模板參數,從而允許用戶控制返回類型:

// 編譯器無法推斷 T1,它未出現在函數參數列表中
template <typename T1,typename T2,typename T3>
T1 sum(T2,T3);

編譯器無法推斷 T1,它未出現在函數參數列表中。所以每次調用 sum 時調用者必須爲 T1 提供一個顯式模板實參

​ 提供顯式模板實參的方式與定義類模板實例的方式相同。在函數名之後,實參列表之前用尖括號指明

auto val3 = sum<long long>(i, lng);		// long long sum(int,long);

顯式模板實參按由左至右的順序與對應的模板參數匹配,即第一個模板實參與第一個模板參數匹配 … 依此類推。只有尾部 (最後) 參數的顯式模板實參纔可以忽略,前提是它們可以從函數參數推斷出來

​ 如我們的 sum 函數如下編寫:

template <typename T1,typename T2,typename T3>
T3 alternative_sum(T2,T1);

我們總是必須提供三個形參指定實參:

auto val3 = alternative_sum<long long>(i, lng);		// 錯誤,T3 的類型未知
auto val2 = alternative_sum<long long,int,long>(i, lng);	// 正確,顯式指定了三個參數
正常類型轉換應用於顯式指定的實參

​ 對於用普通類型定義的函數參數,允許進行正常的類型轉換,出於同樣的原因,對於模板類型參數已經顯式指定了的函數參數,也進行正常的類型轉換

// template <typename T> int compare(const T&,const T&);
long lng;
compare(lng,1024);			// 錯誤,模板參數不匹配
compare<long>(lng,1024);	// 正確,實例化 compare(long,long)
compare<int>(lng,1024);		// 正確,實例化 compare(int,int)

尾置返回類型與類型轉換

​ 當我們希望用戶確定返回類型時,用顯式模板實參表示模板函數的返回類型時很有效的。但在其他情況下,要求顯式指定模板實參會給用戶增添額外負擔。例如,我們可能希望編寫一個函數,接受表示序列的一對迭代器和返回序列中一個元素的引用:

template <typename It>
??? &fcn(It beg,It end) {
    // 處理序列
    return *beg;
}

我們不知道返回結果的精確類型,但知道所需類型是所處理的序列的元素類型:

vector<int> vi = {1,2,3,4,5};
Blob<string> ca = {"hi","bye"};
auto &i = fcn(vi.begin(),vi.end()); // fcn 應該返回 int&
auto &j = fcn(ca.begin(),ca.end()); // fcn 應該返回 string&

我們知道,函數返回 *beg,而我們能夠通過 decltype(*beg) 來獲取此表達式的類型。但是,在編譯器遇到函數的參數列表之前,beg 都是不存在的。所以,爲了定義此類型,我們必須使用尾置返回類型:

template <typename It>
auto fcn(It beg,It end) -> decltype(*beg) {
    // 處理序列
    return *beg;		// 返回序列中一個元素的引用
}
進行類型轉換的標準庫模板類

​ 有時我們無法直接獲得所需要的類型。例如,我們可能希望編寫一個類似 fcn 的函數,但返回一個元素的值而非引用。

​ 在編寫這個函數的過程中,我們面臨一個問題:對於傳遞的參數的類型,我們幾乎一無所知。在此函數中,我們知道唯一可以使用的操作是迭代器操作,而所有迭代器操作都不會生成元素,只能生成元素的引用。

​ 爲了獲得元素類型,我們可以使用標準庫的類型轉換模板。這些模板定義在 type_traits 中。我們可以用個 remove_references 來獲得元素類型。此模板有一個模板類型參數和名爲 type 的 public 類型成員。我們用給一個引用類型實例化 remove_reference,type 將表示被引用的類型。如:remove_references<int&>,其 type 爲 int。

​ 有了這個,我們便可以這樣編寫函數:

template <typename It>
auto fcn(It beg,It end) -> remove_reference<decltype(*beg)>::type {
    // 處理序列
    return *beg;	// 返回序列中一個元素的拷貝
}

標準類型轉換模板還有很多,見 p606

函數指針和實參推斷

​ 當我們用一個函數模板初始化一個函數指針或爲一個函數指針賦值時,編譯器使用指針的類型來推斷模板實參 (對於函數指針來說,其類型還包括形參以及返回類型)。

​ 例如,假定我們有一個函數指針,它指向的函數返回 int,接受兩個實參,每個參數都是指向 const int 的引用。我們可以使用該指針指向 compare 的一個實例:

template <typename T> int compare(const T&,const T&);
// pf1 指向實例 int compare(const int&,const int&);
int (*pf1)(const int&,const int&) = compare;

pf1 中參數的類型決定了 T 的模板實參的類型。在這裏,T 的模板實參類型爲 int。指針 pf1 指向 compare 的 int 版本實例。如果不能從函數指針類型確定模板實參,則產生錯誤:

void func(int(*)(const string&,const string&));
void func(int(*)(const int&,const int&));
func(compare);			// 錯誤,使用 compare 的哪個實例?

在這裏,func 能接受 string 與 int 的實例,所以無法確定 func 的實參的唯一實例化版本,此調用失敗。

​ 我們可以顯式模板實參來消除這個歧義:

func(compare<int>);		// 傳遞 compare(const int&,const int&);

當參數是一個函數模板實例的地址時,程序上下文必須滿足:對每個模板參數,能唯一確定其類型或值。

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