首頁 > 軟體

C++函數模板與過載解析超詳細講解

2022-08-22 14:01:09

1.快速上手

函數模板是通用的函數描述,也就是說,它們使用泛型來定義函數。

#include<iostream>
using namespace std;
template <typename T> 
void Swap(T &a,T &b);//模板原型
struct apple{
    string name;
    double weight;
    int group;
};
void show(apple x);
int main(){
    int a,b;
    a=1;
    b=2;
    Swap(a,b);
    cout<<"a:"<<a<<endl;
    cout<<"b:"<<b<<endl;
    apple c={"Alice",200,1};
    apple d={"Bob",250,2};
    Swap(c,d);
    cout<<"c:"<<endl;
    show(c);
    cout<<"d:"<<endl;
    show(d);
}
template <typename T> 
void Swap(T &a,T &b){
    T temp;
    temp=a;
    a=b;
    b=temp;
}
void show(apple x){
    cout<<"name:"<<x.name<<endl;
    cout<<"weight:"<<x.weight<<endl;
    cout<<"group:"<<x.group<<endl;
}

a:2
b:1
c:
name:Bob
weight:250
group:2
d:
name:Alice
weight:200
group:1

模板函數也可以有原型:

template <typename T>

void Swap(T &a,T &b);

這裡的typename也可以換成class

不過模板原型實際上不常見。

模板函數定義:

template <typename T> 
void Swap(T &a,T &b){
    T temp;
    temp=a;
    a=b;
    b=temp;
}

模板函數隱式範例化:

Swap(a,b); 模板函數會根據實參的型別,給出函數定義。 還有顯式範例化: Swap<int>(a,b); 顯式的定義typename。 對於這兩種範例化,我推薦使用顯式範例化,因為隱式範例化容易出錯。對於這塊知識的詳細解讀,需要有對編譯器有充分的理解,在文章後面會給出。

一般我們不會用到模板函數的原型,因為我們一般把模板函數的定義放在標頭檔案裡面,再需要使用的時候,包含標頭檔案就行了。

不推薦的做法:模板原型放在標頭檔案,模板定義放在cpp檔案裡。

2.過載的模板

如果對函數的過載不瞭解,可以翻看我之前的文章:

行內函式、參照變數、函數過載

模板函數也可以過載,語法和常規函數的過載差不多;被過載的模板函數必須要特徵標不同。

#include<iostream>
using namespace std;
template <typename T> 
void Swap(T &a,T &b);//模板原型
template <typename T>
void Swap(T *a,T *b,int n);//模板原型
struct apple{
    string name;
    double weight;
    int group;
};
void show(apple x);
int main(){
    int a,b;
    a=1;
    b=2;
    Swap(a,b);
    cout<<"a:"<<a<<endl;
    cout<<"b:"<<b<<endl;
    apple c={"Alice",200,1};
    apple d={"Bob",250,2};
    Swap(c,d);
    cout<<"c:"<<endl;
    show(c);
    cout<<"d:"<<endl;
    show(d);
    char e[10]="hello";
    char f[10]="bye!!";
    Swap(e,f,10);
    cout<<"e:"<<e<<endl;
    cout<<"f:"<<f<<endl;
}
template <typename T> 
void Swap(T &a,T &b){
    T temp;
    temp=a;
    a=b;
    b=temp;
}
template <typename T>
void Swap(T *a,T *b,int n){
    T temp;
    for(int i=0;i<n;i++){
        temp=a[i];
        a[i]=b[i];
        b[i]=temp;
    }
}
void show(apple x){
    cout<<"name:"<<x.name<<endl;
    cout<<"weight:"<<x.weight<<endl;
    cout<<"group:"<<x.group<<endl;
}

a:2
b:1
c:
name:Bob
weight:250
group:2
d:
name:Alice
weight:200
group:1
e:bye!!
f:hello

3.模板的侷限性

#include<iostream>
using namespace std;
template<class T>
const T& foo(const T &a,const T &b){
    if(a>b)return a;
    else return b;
}
struct apple{
    string name;
    double weight;
    int group;
};
void show(apple x);
int main(){
    apple c={"Alice",200,1};
    apple d={"Bob",250,2};
    apple max=foo(c,d);
    show(max);
}
void show(apple x){
    cout<<"name:"<<x.name<<endl;
    cout<<"weight:"<<x.weight<<endl;
    cout<<"group:"<<x.group<<endl;
}

上面這段程式碼是出錯的,因為T如果是結構體,我們無法對其做>操作。當然解決這個問題的方法也是有的—顯式具體化函數。

4.顯式具體化函數

顯式具體化函數的誕生是因為模板對於某些型別的資料,定義得的函數,例如上例中得foo(c,d)出錯,我們就單獨對這個型別,寫一個特殊的函數。

所以,就是一句話,原先模板不適用於某種型別的資料,我們就單獨給這種型別的資料,單獨來一個函數定義。

#include<iostream>
using namespace std;
struct apple{
    string name;
    double weight;
    int group;
};
template <typename T> 
void Swap(T &a,T &b);//模板原型
template<>
void Swap<apple>(apple &a,apple &b);//顯式具體化函數原型,這裡<apple>可以省略
void show(apple x);
int main(){
    int a,b;
    a=1;
    b=2;
    Swap(a,b);
    cout<<"a:"<<a<<endl;
    cout<<"b:"<<b<<endl;
    apple c={"Alice",200,1};
    apple d={"Bob",250,2};
    Swap(c,d);
    cout<<"c:"<<endl;
    show(c);
    cout<<"d:"<<endl;
    show(d);
}
template <typename T> 
void Swap(T &a,T &b){
    T temp;
    temp=a;
    a=b;
    b=temp;
}
template<>
void Swap<apple>(apple &a,apple &b){
    cout<<"explicit specialization for apple!"<<endl;
    int temp;
    temp=a.group;
    a.group=b.group;
    b.group=temp;
}
void show(apple x){
    cout<<"name:"<<x.name<<endl;
    cout<<"weight:"<<x.weight<<endl;
    cout<<"group:"<<x.group<<endl;
}

a:2
b:1
explicit specialization for apple!
c:
name:Alice
weight:200
group:2
d:
name:Bob
weight:250
group:1

可以看出來,我們單獨為 結構體apple 搞了個顯式具體化函數,目的就是隻交換group成員變數。

顯式具體化函數和常規模板很類似。

顯式具體化函數的原型:

template<>

void Swap<apple>(apple &a,apple &b);

這裡<apple>可以省略.

顯式具體化函數的定義:

template<>
void Swap<apple>(apple &a,apple &b){
    cout<<"explicit specialization for apple!"<<endl;
    int temp;
    temp=a.group;
    a.group=b.group;
    b.group=temp;
}

實際上這段程式碼也意味著,顯式具體化的優先順序高於常規模板。

5.範例化和具體化

切記!函數模板本身不會生成函數定義,它只是一個生成函數定義的方案!

編譯器使用模板為特定型別生成函數定義時,得到的是模板範例。生成函數定義就是範例化。

範例化有隱式和顯式之分。

隱式範例化:

Swap(a,b);或者Swap<int>(a,b);

隱式範例化是指等你呼叫了這個函數的時候,它才會生成函數定義。

顯式範例化:

template void Swap<int>(int,int);

顯式範例化是指不需要等你呼叫這個函數,使用上面那段程式碼,直接能生成Swap<int>函數的定義。 一般來說,我們會把模板放到一個標頭檔案中,然後很多原始檔會include它,然後編譯的時候就會在這些原始檔中生成具體化的程式碼。但是如果我們採用顯式範例化,在其中一個原始檔裡面範例化一份程式碼,然後其他cpp檔案用到的時候,通過連結程式找到這個程式碼並呼叫它,程式的大小就會少一些。這就是顯式範例化的好處。

下面這段程式碼展示了Add<double>(a,b)相較於Add(a,b)的優越性:

#include<iostream>
using namespace std;
template <typename T>
T Add(const T &a,const T &b){
    return (a+b);
}
int main(){
    int a=5;
    double b=6.1;
    cout<<Add<double>(a,b)<<endl;
}

如果把Add<double>(a,b)換成Add(a,b)會出錯,因為a是int型別的,而b是double型別的,這樣就無法隱式範例化了。Add<double>(a,b)會範例化一個函數定義,然後int型別的a,傳參給double的參照形參的時候,會產生臨時變數,從而完成函數呼叫。總之,最好使用<type>而不是根據引數型別自動生成模板的範例化.

顯式隱式範例化和顯式具體化統稱為具體化或者範例化

上一節中我們提到了顯式具體化,我們可以發現範例化和顯式具體化的相同之處在於,他們都是使用具體型別的函數定義,而不是通用描述。

顯式具體化函數是否是模板? 我的回答是:顯式具體化函數是一個特殊的模板,它是專門為一種型別設計的模板。

//函數模板6.cpp
#include<iostream>
using namespace std;
struct apple{
    string name;
    double weight;
    int group;
};
template<class T>
void Swap(T &a,T &b);//模板函數原型
template<>void Swap(apple &a,apple &b);//顯式具體化原型
template void Swap<char>(char&,char&);//顯式範例化
void show(apple x);
int main(){
    short a=1;
    short b=2;
    Swap(a,b);//隱式範例化
    cout<<"a:"<<a<<endl<<"b:"<<b<<endl;
    apple c={"Alice",200,1};
    apple d={"Bob",250,2};
    Swap(c,d);//顯式具體化
    cout<<"c:"<<endl;
    show(c);
    cout<<"d:"<<endl;
    show(d);
    char e='a';
    char f='b';
    Swap<char>(e,f);//呼叫顯式範例化函數
    cout<<"e:"<<e<<endl<<"f:"<<f<<endl;
}
template<>
void Swap(apple &a,apple &b){
    int temp;
    temp=a.group;
    a.group=b.group;
    b.group=temp;
}
void show(apple x){
    cout<<"name:"<<x.name<<endl;
    cout<<"weight:"<<x.weight<<endl;
    cout<<"group:"<<x.group<<endl;
}
template<class T>
void Swap(T &a,T &b){
    T temp;
    temp=a;
    a=b;
    b=temp;
}

a:2       
b:1       
c:        
name:Alice
weight:200
group:2   
d:        
name:Bob  
weight:250
group:1
e:2.01
f:1 

這裡問個問題,如果把上面程式碼中的e變成 int型別會出現問題嗎?

會報錯,因為實參int和函數中參照形參char&的型別不一樣,且此時不是const參照形參,也不會有臨時變數產生。如果你不清楚,且看參照變數的語法。 行內函式、參照變數、函數過載

6.過載解析

6.1 概覽

對於常規函數,函數過載,函數模板,函數模板過載,編譯器需要有一個良好的策略,從一大堆同名函數中選擇一個最佳函數定義。這一過程是非常複雜的過程–過載解析。這就是我們這一節要闡述的內容。

過載解析過程:

  • step1:建立候選函數列表。其中包含與被呼叫函數名稱相同的函數和模板函數。
  • step2:從候選函數列表中篩選可行函數。其中包括引數正確或者隱式轉換後引數正確的函數。
  • step3:確定是否存在最佳的可行函數。如果有則使用他,否則函數呼叫出錯。

其中最複雜的就是step3,這些可行函數也有優先順序之分,優先順序 從高到低是:

  1. 完全匹配
  2. 提升轉化 (如,char short 轉化成int,float 轉化成 double)
  3. 標準轉化 (如,int 轉化成 char ,long轉化成double)
  4. 使用者定義的轉化 (如類宣告中定義的轉換)

而完全匹配中也有細小的優先順序之分。

總而言之,在step3中如果優先順序最高的可行函數是唯一的那麼就呼叫他,否則會出現諸如ambiguous的錯誤。

這一節的目的就是完全理解編譯器如何讓處理如下程式碼:

#include<iostream>
using namespace std;
void may(int);//#1
float may(float,float=3);//#2存在預設引數
void may(char &);//#3
char* may(const char*);//#4
char may(const char &);//#5
template<class T> void may(const T &);//#6
template<class T> void may(T *);//#7
int main(){
    may('B');
}
void may(int a){
    cout<<1<<endl;
}
float may(float a,float b){
    cout<<2<<endl;
    return a;
}
void may(char &a){
    cout<<3<<endl;
}
char* may(const char* a){
    cout<<4<<endl;
    return NULL;
}
char may(const char &a){
    cout<<5<<endl;
    return a;
}
template<class T> 
void may(const T & a){
    cout<<6<<endl;
}
template<class T> 
void may(T *){
    cout<<7<<endl;
} 

上述程式碼沒有一點問題,甚至連warning都沒有,你可以自己試一下結果是什麼。

'B'是const char型別的

#1~#7都是候選函數,因為函數名字相同。

其中#1、#2、#3、#5、#6是可行函數,因為const char 型別無法隱式轉換成指標型別,所以#4、#7不行,而其他函數通過隱式轉換後引數是正確的。

#1是提升轉換,#2是標準轉換,#3、#5、#6是完全匹配,完全匹配中非模板函數比模板函數優先順序高,所以#3、#5優先順序高於#6,而由於const引數優先和const參照引數匹配,所以#5的優先順序更高。

則#5>#3>#6>#1>#2,所以呼叫#5。

6.2 完全匹配中的三六九等

首先什麼是完全匹配?

完全匹配函數包括:

  • 不需要進行隱式型別轉化的函數(即引數正確的函數)顯然是完全匹配函數。
  • 需要進行隱式型別轉換,但是這些轉換是無關緊要轉換。

完全匹配允許的無關緊要轉換:

實 參形 參
TypeType&
Typc&Type
Type[]* Type
Type (argument-list)Type ( * ) (argument-list)
Typeconst Type
Typevolatile Type
Type *const Type
Type*volatile Type *

完全匹配中的優先順序法則

  • 常規函數優先順序高於模板。
  • 對於形參是指標或參照型別的函數,const修飾的實參優先匹配const修飾的形參,非const修飾的實參優先匹配非const修飾的形參。
  • 較具體的模板優先順序高於較簡略的模板。(例如,顯式具體化函數優先順序高於常規模板)
#include<iostream>
using namespace std;
struct apple{
    string name;
    double weight;
    int group;
};
void may(const apple & a){
    cout<<1<<endl;
}
void may(apple &a){
    cout<<2<<endl;
}
int main(){
    apple a={"Alice",250.00,1};
    may(a);
}

結果是2

#include<iostream>
using namespace std;
struct apple{
    string name;
    double weight;
    int group;
};
void may(const apple & a){
    cout<<1<<endl;
}
void may(apple &a){
    cout<<2<<endl;
}
void may(apple a){
    cout<<3<<endl;
}
int main(){
    apple a={"Alice",250.00,1};
    may(a);
}

這個編譯器會出錯,因為這三個函數都是完全匹配,但是#2 和 #3的優先順序無法區別,記得嗎,完全匹配中的優先順序法則的第2條法則,只適用於形參是參照或者指標。

#include<iostream>
using namespace std;
struct apple{
    string name;
    double weight;
    int group;
};
template<typename T>
void may(T a){
    cout<<1<<endl;
}
template<typename T>
void may(T *a){
    cout<<2<<endl;
}
int main(){
    apple a={"Alice",250.00,1};
    may(&a);
}

終端輸出是2,&a的型別是 apple*,而#2明確指出形參是個指標,所以#2更具體。

關於如何找出最具體的模板的規則被稱為部分排序規則。

部分排序規則:在範例化過程中,函數優先和轉換少的模板匹配。也可以這麼說,實參和形參越相似,模板越優先。

舉個栗子:

#include<iostream>
using namespace std;
template<typename T>
void may(T a[]){
    cout<<1<<endl;
}
template<typename T>
void may(T *a[]){
    cout<<2<<endl;
}
template<typename T>
void may(const T *a[]){
    cout<<3<<endl;
}
int main(){
    double a[5]={1,2,3,4,5};
    const double* b[5]={&a[0],&a[1],&a[2],&a[3],&a[4]};
    may(a);
    may(b);
}

may(a)會和#1匹配,因為a的型別是double陣列,double陣列無法轉換成指標陣列,所以#2,#3不是可行函數。而對於may(b),他會和#3匹配。b的型別是cont指標陣列,首先#1和#2和#3都是可行函數,而且都是完全匹配函數,因為#1 會範例化成may<const double*>(b),#2 他範例化成may<const double>(b),#3會範例化為may<double>(b)所以我們看看那個模板更具體?#3模板直接指出了 形參是一個const指標陣列,所以他最具體,#3優先順序最高;其次是#2因為它的形參指出了是指標陣列;#1是最不具體的,#3>#2>#1.

6.3 總結

可行函數中優先順序從高到低排列  
完全匹配常規函數形參若是指標或參照,注意const和非const
 模板較具體的模板優先順序更高
提升轉換  
標準轉換  
使用者定義轉換  

Swap<>(a,b)這種程式碼,類似於顯式範例化,但是<>中沒有指出typename,所以這段程式碼是要求優先選擇模板函數。

對於多引數的函數,優先順序會非常複雜,就不談了。

7.模板的發展

關鍵字decltype 和 auto

#include<iostream>using namespace std;template<typename T1,typename T2>auto Add(T1 a, T2 b){ decltype(a+b) c; c=a+b; return c;}int main(){ int a=2; double b=2.123; cout<<Add(a,b);}#include<iostream>
using namespace std;
template<typename T1,typename T2>
auto Add(T1 a, T2 b){
    decltype(a+b) c;
    c=a+b;
    return c;
}
int main(){
    int a=2;
    double b=2.123;
    cout<<Add(a,b);
}

關鍵字decltype 和 auto ,在模板中無法確定資料型別時,發揮了巨大的作用。

到此這篇關於C++函數模板與過載解析超詳細講解的文章就介紹到這了,更多相關C++函數模板與過載解析內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com