C++ 函數重載解析策略
- 2022 年 10 月 20 日
- 筆記
- C++, C++ 基礎知識雜記
參考《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 步:確定最佳匹配函數。通常,從最佳到最差的順序如下所述:
- 特徵標完全匹配;
- 類型需經隱式提升轉換,例如
char
和short
自動轉換為int
,float
自動轉換為double
; - 類型需經隱式標準轉換,例如
int
轉換為char
,long
轉換為double
; - 類型需經隱式自定義轉換,例如類中用戶定義的類型轉換。
依此規則,函數 #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
;第二次調用時根據重載解析策略選擇了模板函數 #1
的 double
版本,屬於模板函數的隱式實例化;第三次調用的 <>
指出,編譯器應該選擇模板函數,此時編譯器會查看調用函數時的實參類型來進行實例化,也屬於模板函數的隱式實例化;第四次調用的 <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;
}