C++ 函式過載,函式模板和函式模板過載,選擇哪一個?
過載解析
在C++中,對於函式過載、函式模板和函式模板過載,C++需要有一個良好的策略,去選擇呼叫哪一個函式定義(尤其是多個引數時),這個過程稱為過載解析。
(這個過程將會非常複雜,但願不要遇到一定要寫這種程式碼的時候。)
大致步驟
Ⅰ.建立候選函式列表(其中包含與候選函式相同名稱的函式和模板函式)。
Ⅱ.使用候選函式列表建立可行函式列表(要求引數數目正確,為此有一個隱式型別轉換序列,其中包括實參型別與相應的形參型別完全匹配的情況。例如,使用float引數的函式呼叫可以將該引數轉換為double型別,從而與double匹配,而模板函式可以為float型別生成一個函式例項)。
Ⅲ.確定是否有最佳的可行函式(如果有則呼叫,沒有則報錯)。
我們以只有一個引數的函式為例:
1 may('B'); // 函式呼叫 2 3 /*以下是一系列候選函式宣告*/ 4 void may(int); // #1 5 float may(float, float = 3); // #2 6 void may(char); // #3 7 char * may(const char *); // #4 8 char may(const char &); // #5 9 template<class T> void may(const T &); // #6 10 template<class T> void may(T *); // #7
這些函式宣告都會進入函式列表(因為名稱相同),接下來考慮特徵標(引數數量與型別),不考慮返回值。其中#4和#7不可行,因為整數無法隱式型別轉換為指標型別。#6可用來生成具體化,其中T被替換為char型別,此時還剩下5個可行的函式(#1、#2、#3、#5、#6)。如果此時只剩下一個,那麼任何一個都可以正確使用。
接下來,到了擇優環節。這一步主要考量的是函式呼叫引數與可行的候選函式的引數匹配所需要進行的轉換。通常,從最佳到最差的順序如下:
1、完全匹配,函式優於模板。
2、提升轉換(例如,char和short自動轉換為int,float自動轉換為double)。
3、標準轉換(例如,int轉換為char,long轉換為double)。
4、使用者定義的轉換,如類宣告中定義的轉換。
在剩餘的5個函式中,#1優於#2,因為char到int是提升轉換,而char到float是標準轉換(此時還剩#1,#3,#5,#6)。#3、#5、#6優於#1和#2,因為他們是完全匹配(還剩#3,#5,#6)。#3和#5優於#6,因為#6是模板(還剩#3和#5)。
這時,會出現兩個問題,完全匹配到底是什麼?如果有兩個完全匹配(#3和#5)該怎麼辦?通常有兩個完全匹配是一種錯誤,但這一規則有兩個例外。
完全匹配和最佳匹配
進行完全匹配時,C++允許某些“無關緊要的轉換”,下表列出了這些轉換——Type表示任意型別。例如,int到int &,注意,Type可以是char &這樣的型別,因此,這些規則也包括char &到const char &的轉換。
從實參 | 到形參 |
Type | Type & |
Type & | Type |
Type[] | * Type |
Type(引數列表) | Type(*)(引數列表) |
Type | const Type |
Type | volatile Type |
Type * | const Type |
Type * | volatile Type * |
假設有如下程式碼:
1 struct blot {int a; char b[10]}; 2 blot ink = {25, "spots"}; 3 recycle(ink); 4 5 // 下面的原型完全匹配 6 void recycle(blot); // #1 blot to blot 7 void recycle(const blot); // #2 blot to const blot 8 void recycle(blot &); // #3 blot to blot & 9 void recycle(const blot &); // #4 blot to const blot &
如果有多個完全匹配的原型,則無法完成過載解析過程,如果沒有最最佳的可行函式,編譯器將報錯。
然而,有這樣的例外規則,首先,指向非const資料的指標和引用優先於非const指標和引用,在上例中,如果只定義了#3和#4,將選擇#3,因為ink沒有被宣告為const,然而const和非const之間的區別只適用於指標和引用指向的資料,也就是說,如果只定義了#1和#2,將出現二義性錯誤。
一個完全匹配優於另一個的另一種情況是,其中一個是非模板函式而另一個不是,這種情況下,非模板函式將優先於模板函式(包括顯式具體化)。
如果兩個完全匹配的函式都是模板函式,則較具體的模板函式優先,這意味著顯示具體化將優於模板隱式生成的具體化。
1 struct blot {int a; char b[10]}; 2 template <class Type> void recycle(Type t); // 模板 3 template <> void recycle<blot>(blot & t); // 顯示具體化 4 5 blot ink = {25, "spots"}; 6 recycle(ink); //使用顯示具體化
術語“最具體”並不一定意味著顯示具體化,而是指編譯器推斷使用哪種型別時執行的轉換最少。例如:
1 struct blot {int a; char b[10]}; 2 template <class Type> void recycle(Type t); // #1 3 template <class Type> void recycle(Type * t); // #2 4 5 blot ink = {25, "spots"}; 6 recycle(&ink); // 使用#2,因為轉換最少,#2被認為是更具體的
用於找出最具體的模板的規則被稱為部分排序規則。
部分排序規則
1 template <typename T> 2 void show(T arr[], int n); // #1 3 4 template <typename T> 5 void show(T * arr[], int n); // #2 6 7 struct debts 8 { 9 char name[50]; 10 double amount; 11 }; 12 13 ...... 14 15 int things[6] = {13,31,103,301,310,130}; 16 debts mr[3] = 17 { 18 {"aaa", 24.1}, 19 {"bbb", 25.2}, 20 {"ccc", 26.3} 21 }; 22 double * pd[3]; 23 24 for(int i=0; i<3; i++) 25 { 26 pd[i] = &mr[i].amount; 27 } 28 29 show(things, 6); // 使用#1 30 show(pd, 3); // 使用#2
things是一個int陣列,與#1匹配,其中T被替換為int。pd是一個double *陣列,與#1匹配時,T被替換為double *,與#2匹配時,T被替換為double。在這兩個模板中,#2更加具體,因為它做了特定的假設,陣列內容是指標,因此被使用。如果將#2從程式中刪除,那麼使用#1,將顯示出地址,而不是值。
總之,過載解析將尋找最匹配的函式,如果只存在一個這樣的函式,則選擇它;如果存在多個這樣的函式,但其中只有一個非模板函式,則選擇它;入伏哦存在多個合適的函式且都為模板函式,但其中只有一個函式比其他函式更具體,則選擇它。其他情況(有多個非模板或模板函式,但沒有一個比其他更具體,或根本不存在匹配的函式)均為錯誤。
建立自定義選擇
在有些情況下,可以引導編譯器做出你希望的選擇。
1 template<class T> 2 T lesser(T a, T b); // #1 3 4 int lesser(int a, int b); // #2 5 6 ...... 7 8 int m = 20; 9 int n = -30; 10 double x = 15.5; 11 double y = 25.9; 12 13 lesser(m, n); // 使用#2 14 lesser(x, y); // 使用#1,T被轉換為double型別 15 lesser<>(m, n); // <>提示編譯器,使用模板函式,使用#1 16 lesser<int>(x, y); // 顯式例項化,將使用例項化後的函式x,y被強制轉換為int型別
多個引數的函式
將有多個引數的函式呼叫與有多個引數的原型進行匹配時,情況將非常複雜。編譯器必須考慮所有引數的匹配情況。如果找到比其他可行函式都合適的函式,則選擇該函式。一個函式要比其他函式都合適,其所有引數的匹配程度都必須不必其他函式差,同時至少有一個引數的匹配程度比其他函式