C++ 函數過載解析策略

2022-10-20 09:00:19

參考《C++ Primer Plus》(第6版)中文版,Stephen Prata 著,張海龍 袁國忠譯,人民郵電出版社。C++ 使用過載解析策略來決定為函數呼叫使用哪一個函數定義。過載解析過程大致分為如下三步:

  • 第 1 步:建立候選函數列表,只要求函數名一樣即可,對函數特徵標以及是否為模板函數無要求;
  • 第 2 步:在上一步的基礎上建立可行函數列表,包含特徵標完全匹配的常規函數或模板函數、以及實參隱式轉換後完全匹配的常規函數或模板函數,這些都是引數數目正確的函數;
  • 第 3 步:在上一步的基礎上確定最佳匹配函數,若有則使用它,若沒有則該函數呼叫失敗。

下面以一個例子來說明這個過載過程:

//全部函數原型
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
void may(char, double);               //原型#8
void mbk(float);                      //原型#9
char mkk(int, char);                  //原型#10
int mck(char);                        //原型#11
double myk(float);                    //原型#12
void mpk(char);                       //原型#13

//函數呼叫
may('B');

//函數定義
...

過載第 1 步:建立候選函數列表。即函數名稱為 may 的常規函數和模板函數,候選函數列表如下:

//過載第1步:建立候選函數列表
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
void may(char, double);               //原型#8

過載第 2 步:建立可行函數列表。由於整數型別 char 不能被隱式地轉換為指標型別 char *,因此函數 #4 和函數 #7 都被排除,而函數 #8 因為引數數目不匹配也會被排除。進行完全匹配時,C++ 允許下表這些無關緊要的轉換,表中 Type 表示任意型別,例如 char &const char & 的轉換也包含在內,表中 Type (argument-list) 意味著用作實參的函數名和用作形參的函數指標只要返回型別和參數列相同,就是匹配的。

實參型別 形參型別
Type Type &
Type & Type
Type [] Type *
Type (argument-list) Type (*) (argument-list)
Type const Type
Type volatile Type
Type * const Type *
Type * volatile Type *

根據此表可知,剩下的函數中包含特徵標完全匹配的常規函數 #3#5、特徵標完全匹配的模板函數 #6(此時 T 可以被範例化為 char)、實參隱式轉換後完全匹配的常規函數 #1#2。可行函數列表如下:

//過載第2步:建立可行函數列表
void may(int);                        //原型#1
float may(float, float = 3);          //原型#2
void may(char);                       //原型#3
char may(const char &);               //原型#5
template<class T> void may(const T &);//原型#6

過載第 3 步:確定最佳匹配函數。通常,從最佳到最差的順序如下所述:

  1. 特徵標完全匹配
  2. 型別需經隱式提升轉換,例如 charshort 自動轉換為 intfloat 自動轉換為 double
  3. 型別需經隱式標準轉換,例如 int 轉換為 charlong 轉換為 double
  4. 型別需經隱式自定義轉換,例如類中使用者定義的型別轉換。

依此規則,函數 #3 和函數 #5、函數 #6 都是特徵標完全匹配的最佳匹配函數,函數 #1 需經隱式提升轉換,函數 #2 需經隱式標準轉換,由此各函數最佳匹配程度為:(#3, #5, #6) > #1 > #2。當特徵標完全匹配時,又有如下規則:

  • 指向非 const 資料的指標和參照優先與形參為非 const 指標和參照的函數匹配;
  • 優先與非模板函數匹配;
  • 同為模板函數時,優先與較具體的模板函數匹配。

依此規則,非模板函數 #3#5 最佳匹配程度要高於模板函數 #6 ,即各函數最佳匹配程度為:(#3, #5) > #6 > #1 > #2。最終出現了兩個最佳匹配函數 #3#5 ,因此該函數呼叫失敗,編譯器將報錯

//過載第 3 步:確定最佳匹配函數
void may(char);                       //原型#3
char may(const char &);               //原型#5

下面展開來說上述幾條完全匹配時的規則。

第 1 條:指向非 const 資料的指標和參照優先與形參為非 const 指標和參照的函數匹配,這一點需明確,const 和非 const 之間的區別只適用於指標和參照。下面 4 個函數都與函數呼叫是完全匹配的:

//函數原型
void recycle(int);        //原型#1
void recycle(const int);  //原型#2
void recycle(int &);      //原型#3
void recycle(const int &);//原型#4

//函數呼叫
int x = 5;
recycle(x);

//函數定義
...
  • 如果這 4 個函數同時存在,則無法完成過載,編譯器會報多義性匹配的錯誤;
  • 如果只存在函數 #1#2,則無法完成過載,編譯器會報重複定義的錯誤;
  • 如果只存在函數 #1#3,則無法完成過載,編譯器會報多義性匹配的錯誤;
  • 如果只存在函數 #1#4,則無法完成過載,編譯器會報多義性匹配的錯誤;
  • 如果只存在函數 #2#3,則無法完成過載,編譯器會報多義性匹配的錯誤;
  • 如果只存在函數 #2#4,則無法完成過載,編譯器會報多義性匹配的錯誤;
  • 如果只存在函數 #3#4,則函數呼叫時編譯器將會選擇 #3

第 2 條:優先與非模板函數匹配,這一點比較簡單,當完全匹配的函數中,一個是非模板函數,另一個是模板函數時,非模板函數將優於模板函數,顯式具體化、顯式範例化、隱式範例化都屬於模板函數。

第 3 條:同為模板函數時,優先與較具體的模板函數匹配,找出最具體的模板的規則被稱為函數模板的部分排序規則(partial ordering rules)。這意味著顯式具體化優先於常規模板函數,都為常規模板函數時,編譯器優先選擇範例化時型別轉換更少的那一個。以下面的程式為例,呼叫方式 recycle(&ink) 既與模板 #1 匹配,此時 Type 將被解釋為 blot *,也與模板 #2 匹配,此時 Type 將被解釋為 blot,因此將這兩個隱式範例 recycle<blot *>(blot *)recycle<blot>(blot *) 傳送到可行函數池中。在選擇最佳匹配函數時,#2 被認為是更具體的,因為它已經顯式地指出,函數引數是指向 Type 的指標,相比於 #1,它對型別的要求更加地具體,在生成過程中所需要的轉換更少,因此呼叫方式 recycle(&ink) 實際會匹配版本 #2

//兩個常規模板函數
template <class Type> void recycle(Type t);   //原型#1
template <class Type> void recycle(Type * t); //原型#2

//呼叫程式包含如下程式碼
struct blot {int a; char b[10];};
blot ink = {25, "spots"};
...
recycle(&ink);  //使用版本#2

//函數定義
...

部分排序規則的另一個範例程式如下,它與上一個例子有異曲同工之妙。由於模板 #2 做了特定的假設:陣列內容是指標,對型別的要求更加地具體,因此在呼叫時第一個引數若傳入指標陣列 pt,則將實際匹配函數 #2

//兩個常規模板函數
template <typename T> 
void ShowArray(T arr[], int n);   //原型#1
template <typename T> 
void ShowArray(T * arr[], int n); //原型#2

//呼叫程式包含如下程式碼
int things[6] = {13, 31, 103, 301, 310, 130};
int * pt[3] = {&things[0], &things[2], &things[4]};
ShowArray(things, 6);  //使用版本#1
ShowArray(pt, 3);      //使用版本#2

//函數定義
...

將有多個引數的函數呼叫與有多個引數的原型進行匹配時,編譯器必須考慮所有引數的匹配情況。如果找到比其他可行函數都合適的函數,則選擇該函數。一個函數要比其他函數都合適,其所有引數的匹配程度都必須不比其他函數差,同時至少有一個引數的匹配程度比其他函數都高。

在有些情況下,可通過編寫合適的函數呼叫,來引導編譯器做出程式設計師期望的選擇。如下所示,其中模板函數返回兩個值中較小的一個,非模板函數返回兩個值中絕對值較小的那個。第一次呼叫時根據過載解析策略選擇了非模板函數 #2;第二次呼叫時根據過載解析策略選擇了模板函數 #1double 版本,屬於模板函數的隱式範例化;第三次呼叫的 <> 指出,編譯器應該選擇模板函數,此時編譯器會檢視呼叫函數時的實參型別來進行範例化,也屬於模板函數的隱式範例化;第四次呼叫的 <int> 顯式指出,編譯器應該使用模板函數的 int 範例化版本,此時屬於模板函數的顯式範例化。

#include <iostream>

//函數#1
template<class T>
T lesser(T a, T b)
{
    return a < b ? a : b;
}

//函數#2
int lesser(int a, int b)
{
    a = a < 0 ? -a : a;
    b = b < 0 ? -b : b;
    return a < b ? a : b;
}

//函數呼叫
int main()
{
    using namespace std;
    
    int m = 20;
    int n = -30;
    double x = 15.5;
    double y = 25.9;
    
    //使用#2,結果為20
    cout << lesser(m, n) << endl;
    
    //使用#1,double隱式範例化,結果為15.5
    cout << lesser(x, y) << endl;
    
    //使用#1,int隱式範例化,結果為-30
    cout << lesser<>(m, n) << endl;
    
    //使用#1,int顯式範例化,結果為15
    cout << lesser<int>(x, y) << endl;
    
    return 0;
}