詳解C++中函數模板的定義與使用

1. 前言

什麼是函數模板?

理解什麼是函數模板,須先搞清楚為什麼需要函數模板。

如果現在有一個需求,要求編寫一個求 2 個數字中最小數字的函數,這 2 個數字可以是 int類型,可以是 float 類型,可以是所有可以進行比較的數據類型……

常規編寫方案:針對不同的數據類型編寫不同的函數。

#include <iostream>
using namespace std;
//針對 int 類型 
int getMin(int num1,int num2) {
   return num1>num2?num2:num1; 
} 
//針對  float 類型
float getMin(float num1,float num2) {
   return num1>num2?num2:num1; 
}
//針對 double 類型 
double getMin(double num1,double num2) {
   return num1>num2?num2:num1; 
}  

int main() {
    //整型數據比較
    int min=getMin(10,4);
    cout<<min<<endl;
    //float 類型數據比較
    float minf=getMin(3.8f,2.9f);
	cout<<minf<<endl; 
    //double 類型數據比較
	double mind=getMin(1.8,2.1);
	cout<<mind<<endl; 
	return 0;
}

重載函數(當然上述幾個函數名也可以不相同)可以解決這個問題。顯然,上述 3 個函數的算法邏輯是一模一樣的,僅是函數的參數類型不一樣。既然函數的形式參數可以接受值不同的同類型數據,能否把函數形參的數據類型也參數化,用來接受不同的數據類型。

函數模板實質就是參數化數據類型,稱這種編程模式為數據類型泛化編程。

Tips: 泛化的意思是一般化、抽象化,先不明確指定,需要時再指定。

如:我對班長說,我需要一名學生幫我搬課桌。這名學生到底是誰,我沒有明確,由班長具體化。換在函數模板中,表示函數模板需要一種數據類型的數據,具體是什麼數據類型,由使用者決定。

2. 初識函數模板

2.1 語法

在重構上述代碼時,先瞭解一下函數模板的語法結構:

template <模板形式參數列表>  返回類型 函數名(函數形式參數列表)
{
函數體
}

語法結構說明:

1.template關鍵字說明瞭此函數是一個函數模板。

2.template <>的尖括號裡是模板參數列表,也可稱此處的參數為數據類型參數,用來對函數算法所針對的數據類型的泛化,表示可以接受不同的數據類型。

Tips:模板參數列表中的參數可以是一個或多個泛化數據類型參數,也可以是一個或多個具體數據類型參數。

泛化類型參數前面要加上 typename 關鍵字。

3.後面便是函數的一般性說明,隻是在函數中可以使用模板數據類型參數。

Tips: 函數模板中有 2 類參數,模板參數和函數參數。

使用函數模板重構上面求最小值的代碼:

template<typename T> T getMin(T num1,T num2){
	return num1>num2?num2:num1; 
} 

說明:

1.typename T聲明瞭一個數據類型參數,用於泛化任一種數據類型,或者說 T可以表示任意一種數據類型。

Tips:typename 是 C++11 標準,也可以使用 class關鍵字,但建議不用,避免和類定義混淆。

2.T數據類型可以作為函數的參數類型、返回值類型、以及作為算法實施過程中臨時變量的數據類型。

Tips: T是一個變量標識符,在遵循變量命名規則的前提下,可以起任意名稱。

2.2 實例化

函數模板如現實生活中制作陶瓷的模具一樣,隻有往模具中註入原材料,才能生成可實用的陶瓷。函數模板不是函數,僅是一個模板,不能直接調用,需要實例化後才能調用。

實例化:指編譯器根據開發者對函數模板註入的具體(實參)數據類型構造出一個真正的函數實體(實例),這個過程由編譯器自動完成,且實例化的函數對於開發者不可見。

int res= getMin<int>(1,6);
cout<<res<<endl;
//輸出結果:1

如上,編譯器通過函數模板<>內的int數據類型,實例化的函數可以對 int類型的數據進行算法操作。同理,下面的代碼會讓編譯器實例化針對不同數據類型的數據進行算法操作的函數。

//實例化原型為 float  getMin(float num1,float num2){函數體} 的函數
float resf=getMin<float>(3.2f,8.2f);
cout<<resf<<endl;
//實例化原型為 double  getMin(double num1,double num2){函數體} 的函數
double resd=getMin<double>(1.2,0.2);
cout<<resd<<endl;
//實例化原型為 char  getMin(char num1,char num2){函數體} 的函數
char resc=getMin<char>('A','B');
cout<<resc<<endl;
//輸出結果分別為  3.2f  0.2  A 

使用函數模板的優點不言而喻,聲明一次,便可以實現針對不同數據類型的數據的操作。當然,中間會有匹配、實例化的代價。

Tips:高級業務層面的一勞永逸往往會以犧牲底層的性能為代價,但是,這是值得的。

除瞭通過顯示聲明數據類型提示編譯器實例化,也可以使用函數指針實例化。

typedef int(*PF)(int,int); // 1 
PF pf=getMin;  // 2
int res= pf(6,8);  //3
cout<<res;  //4

說明:

處先定義一個函數指針類型。

處這行代碼,千萬不要理解是取函數模板的地址,編譯器在底層做瞭相應處理。

編譯器會根據函數指針類型說明先實例化一個函數。

再取實例化函數的內存地址,並賦值給 pf

3 處以函數指針方式調用函數。

實例化時要註意的幾個問題:

1.實例化時,可能會有一個直觀問題:真的能指定任意一種數據類型實例化函數模板嗎?

答案是:任何高級層面的邏輯行為都不能脫離基礎知識的認知范疇,不同的數據類型有著語法系統賦予它的運算操作能力,當指定一個不支持函數模板內部算法操作的數據類型時,必然會出錯。

如聲明一個求 2 個數字相除的餘數的函數模板。

template<typename T> T getYuShu(T num1,T num2) {
	return num1 % num2;
}

如果指定 double 數據類型實例化 getYuShu 函數模板時,就會拋出錯誤,因為 double數據類型不能使用 %運算符。

double res=getYuShu<double>(6.2,2.4);  //出錯

Tips: 編譯器在實例化函數模板時,會遵循語法標準檢查給定的數據類型是否支持函數模板中的運算操作。

2.編譯器實例化的時機。

常規而言,編譯器會在程序中第一次需要函數模板的某個實例時對其進行編譯。但是,同一份代碼中,可能會出現對同一個實例多次調用的需要,如下面的代碼:

template <typename T > test(T num) {
	return num;
}
int f() {
	int res= test<int>(12);
	return res;
}
double f1() {
	int res= test<int>(24);
	return double(res);
}

ff1函數都需要使用 test<int>實例,於編譯器而,無法知道 ff1函數誰先會被調用(也就無法確定第一次編譯的時間點),但為瞭保證編譯期間完成實例化工作,早期C++編譯器采用對同一實例每一次出現的地方都編譯的策略,然後從多個編譯結果中選一個作為最終結果,顯然,編譯時間會大大延長。

C++充許顯式實例化聲明,用來顯示指定某一個函數模板的實例化的時間點,從而解決同一個實例被多次編譯的問題。其語法如下:

template 返回值類型 模板名<模板參數列表>(函數形參列表);

針對上述函數模板可以編寫如下代碼,告之編譯器編譯時間點。

template <typename T > test(T num) {
	return num;
}
//顯示指定實例化
template int test<int>(int);

Tips: 顯示聲明隻對一個源文件有效。

2.3 實參推導

所謂實參推導,在使用函數模板時省略<>,不明確指定數據類型參數,而是由編譯器根據函數的實參類型自動推導出類型參數的真正類型。如下代碼:

int res=getMin(4,7);

實參是int 類型, 編譯器由此推導出 T 是 int類型,從而使用 int類型實例化函數模板,類似於下面的顯示聲明代碼:

int res=getMin<int>(4,7);

實參推導可以像調用普通函數一樣使用函數模板。但是實參推導是有前提條件的:函數參數使用瞭類型參數的才能通過函數實參類型推導。如下的函數模板。

template <typename T1,typename T2> T2 myMax(T1 num1,T1 num2) {
	//函數體
}

因為 T2是作為函數模板的返回類型,是無法通過實參類型推導出來的。如下圖所示:

使用如上函數模板,需要顯示指定具體的數據類型。

double res= myMax<int,double>(6,8); //正確

是否可以讓函數模板的類型參數一部分顯示指定,一部分由實參推導?

答案是可以,但是,要求在聲明函數模板時,把需要顯示指定的類型參數放在前面,可由實參推導的參數類型放在後面。把上面的函數模板的 T1、T2參數說明交換位置。

template <typename T2,typename T1> T2 myMax(T1 num1,T1 num2) {
	//函數體
}

實例化時,隻需要顯示指定 T2的類型,T1類型由編譯器根據實參推導。如下代碼可正確調用。

double res= myMax<double>(6,8); //正確

編譯器把 T2指定為 double類型,然後根據實參68推導出 T1是 int類型。

瞭解什麼是實參推導後,使用時,需要知道實參推導是不支持自動類型轉換的。如下代碼是錯誤的。

int res=getMin(4,7.5); //錯誤

編譯器認定實參 4int類型,實參7.5是 double類型,那麼是到底是使用 int 類型還是使用 double類型實例化 getMin 函數模板,會讓編譯器不知所措、左右為難。

Tips: 即使支持自動類型轉換,於編譯器而言也無法知道開發者是想使用 int 類型還是 double 類型。如此自動類型轉換沒有存在的意義。

對於上述問題可以采用如下幾種方案解決:

1.通過強制類型操作把實參轉換成統一數據類型。

int res=getMin(4,int(7.5));
//或者
int res=getMin(double(4),7.5);

2.顯示指定實例化時的數據類型。

int res=getMin<int>(4,7.5);
//或者
int res=getMin<double>(4,7.5);

3.如果有必要傳遞 2 個不同類型的參數,可需要修改函數模板,使其能接受 2 種類型參數。

template<typename T1,typename T2> T1 getMin(T1 num1,T2 num2){
	return num1>num2?num2:num1; 
} 

3. 重載函數模板

C++中普通函數和函數模板可以一起重載,面對多個重載函數,編譯器需要提供相應的匹配策略。如下代碼:

//普通函數
int getMax(int num1,int num2){
	return num1>num2?num1:num2; 
} 
//函數模板
template<typename T> T getMax(T num1,T num2) {
	return num1>num2?num1:num2;
}

如下調用時,編譯器是選擇普通函數還是函數模板?

int res= getMax(6,8);

函數實參是 int類型,相比較函數模板,普通函數不需要實例化可直接使用,編譯器會優先選擇普通函數。但是如下的調用,編譯器會選擇函數模板。

getMax(2.4,6.8); //調用 getMax<double>(實參推導)
getMax('a','b'); //調用 getMax<char>(實參推導)
getMax<>(7,3) //調用 getMax<int> (實參推導)
getMax<double>(4,9) //顯示指定

編譯器選擇函數模板的原則:

如果函數模板能實例出一個完全與函數實參類型相匹配的函數,那麼就會選擇函數模板,如getMax(2.4,6.8); 調用。編譯器會根據函數模板實例化一個double getMax(double a,double b)函數與需求完全相匹配的函數。

如果即想使用實參推導,且想使用函數模板而非普通函數,可以使用空 <>尖括號語法。如上的 getMax<>(7,7);調用。一旦指定<>標識符,顯示指定使用函數模板,無論其中是否有實參類型說明。

如下的函數調用,實參有 2 個,但 2者之間可以發生自動類型轉換。

charint之間可以相互轉換。

getMax('a',98);

編譯器會選擇誰?可以做一個實驗,把普通函數註釋,保留函數模板。

#include <iostream>
#include <cstring>
using namespace std;
//函數模板
template<typename T> T getMax(T num1,T num2) {
	return num1>num2?num1:num2;
}
int main(int argc, char** argv) {
    int t= getMax('a',98)
	return 0;
}

執行後:

再恢復普通函數後執行,代碼可以正常執行。顯然,編譯器選擇的是普通函數。原因很簡單,在使用實參推導時,函數模板是不支持自動類型轉換,而普通函數表示沒有壓力。

總結一下,選擇時,編譯器會先考慮有沒有類型完全相匹配的普通函數,沒有,試著看能不能實例化一個完全匹配的函數。

以上就是詳解C++中函數模板的定義與使用的詳細內容,更多關於C++函數模板的資料請關註WalkonNet其它相關文章!

推薦閱讀: