Q:当有多种匹配时,编译器如何选择合适的函数版本?
A:对于函数重载、函数模板 和 函数模板重载,我们会看到一个实参可以匹配多个函数,那么编译器将选择哪一个匹配版本呢?编译器分析的这个过程称为重载解析(overloading resolution),这个过程大致分为下面几个步骤:
第 1 步:创建候选函数列表。其中包括与被调用函数名称相同的函数和模板函数。
第 2 步:使用候选函数列表创建可行函数列表。这里,要保证参数数目正确,实参类型与相应形参完全匹配(通过转换达到匹配也可以,如使 float 型的实参可转换到 double 类型的形参,从而匹配)。
第 3 步:确定是否有最佳的可行函数。如果有,就使用它,否则调用出错。
下面声明一组函数:
[C++] 纯文本查看 复制代码
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
如果像下面调用函数:如果只考虑参数特征,而不考虑返回类型,首先排除 #4 和 #7 函数,因为 char 类型不能被隐式地转换到指针类型,并且这里也没有强制类型转换。其它的 5 个函数,都可以被使用,但是编译器必须决定这些可行函数中哪一个是最佳的。一般的,从最佳到最差的顺序如下:
1. 完全匹配,但常规函数优先于模板。
2. 提升转换 (例如,char 和 shorts 自动转换为 int,float 自动转换为 double)。
3. 标准转换 (例如,int 转换为 char,long 转换为 double)。
4. 用户定义的转换,如类声明中定义的转换。
按照上面所述,
#1 比 #2 的优先级高,因为 char 到 int 的转换是提升转换,而 char 到 float 的转换是标准转换。
#3,#5 和 #6 优先于 #1 和 #2 ,因为它们的参数是完全匹配的。
#3,#5 又优先于 #6,因为 #6 是模板。
如果 #3 和 #5 都完全匹配,那该如何?一般来说,有两个函数完全匹配是一种错误。但这规则有两个意外。
完全匹配与最佳匹配
进行完全匹配时,C++ 允许某些“无关紧要的转换”。下表列出了这些转换。其中 Type 表示任意类型。比如,int 实参与 int & 形参完全匹配。注意,Type 还可以表示 char & 这样的类型,因此这些规则包括从 char & 到 const char & 的转换。Type (argument-list) 表示的是实参是一个函数,并且返回的是 Type 类型,它与用作形参的 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 * |
假设有下面的代码:- struct blot { int a; char b[10]; };
- blot ink = {25, "sports"};
- ...
- recycle(ink);
复制代码 在这种情况下,下面的原型都是完全匹配的:
[C++] 纯文本查看 复制代码 void recycle(blot); // #1 blot-to-blot
void recycle(const blot); // #2 blot-to-(const blot)
void recycle(blot &); // #3 blot-to-(blot &)
void recycle(const blot &); // #4 blot-to-(const blot &)
如果有多个匹配的原型,编译器就无法完成重载解析过程;如果没有最佳的可行函数,编译器将产生一条错误信息,比如出现 "ambiguous(二义性)" 这样的提示:
但是,有时候,即使两个函数都完全匹配,仍可完成重载解析。首先,指向非 const 数据的指针和引用要优先于非 const 指针和引用参数匹配。如在 recycle() 中,如果只定义了 #3 和 #4 ,那么编译器会选择 #3,因为 ink 没有被声明为 const 。然而,const 和非 const 之间的区别只适用于指针和引用指向的数据。也就是说,如果只定义了 #1 和 #2,那么还会出现二义性错误。只针对指针或引用的情况有用,其原因是指针或引用指向的是原数据,而按值传递则针对的是一个副本;而对于副本,不同的原型可能具有不同的目的,但它不会改变原数据。换作是指针或引用,const 版本不会修改原数据,非 const 版本则有可能会修改数据,这里目的只有一个---对原数据操作---要么改变,要么不改变,而改变目的的要优先于不改变的,即非 const 优先于 const 。
一个完全匹配优于另一个的另一种情况是,其中一个是非模板函数,而另一个不是。在这种情况下,非模板参数将优先于模板参数(包括显式具体化)。
如果两个完全匹配的函数都是模板函数,则教具体的模板函数有限。这意味着,显式具体化将优先于使用模板隐式生成的具体化:- struct blot {int a; char b[10];};
- template <class Type> void recycle (Type t); // 模板
- template <> void recycle<blot> (blot & t); // 对 blot 具体化
- ...
- blot ink = {25, "spots"};
- ...
- recycle(ink); // use 具体化
复制代码 术语 “最具体”(most specialized) 并不一定意味着显式具体化,而是指编译器推断使用哪种类型时执行的转换最少。例如,下面的两个模板:- template <class Type> void rcycle (Type t); #1
- template <class Type> void recycle (Type * t); #1
复制代码 假设包含这些模板的程序也包含下面的代码:- struct blot {int a; char b[10];};
- blot ink = {25, "spots"};
- ...
- recycle(&ink); // 结构地址
复制代码 recycle(&ink) 调用与模板 #1 匹配,匹配时将 Type 解释为 blot * 。
recycle(&ink) 也与模板 #2 匹配,此时 Type 被解释为 ink 。
因此,两个隐式示例 ---- recylce<blot *>(blot *) 和 recycle<blot>(blot *) 被发送到可行函数池中。
在这两个模板函数中,recycle<blot *>(blot *) 被认为更具体。这是因为在生成的过程中,它需要进行的转换更少。也就是说,#2 模板已经显式指出,函数参数是指向 Type 的指针,因此可以直接用 blot 标识 Type。而 #1 模板将 Type 作为函数参数,因此 Type 必须被解释为指向 blot 的指针,也就是说,在 #2 中,Type 已经被具体化为指针,因此说它“更具体”。
用于找出最具体的模板的规则被称为函数模板的部分排序规则(partial ordering rules)。
部分排序规则示例
下面程序中,使用了部分排序规则来确定要使用哪个模板定义。在改程序中,有两个用来显示数组内容的模板定义。第一个定义(模板 A),假设作为参数传递的数组中包含了要显示的数据;第二个定义(模板 B)假设数组元素为指针,指向要显示的数据。
代码:
[C++] 纯文本查看 复制代码
#include <iostream>
template <typename T> // template A
void ShowArray(T arr[], int n);
template <typename T> // template B
void ShowArray(T * arr[], int n);
struct debts
{
char name[50];
double amount;
};
int main()
{
using namespace std;
int things[6] = {13, 31, 103, 301, 310, 130};
struct debts mr_E[3] =
{
{"Ima Wolfe", 2400.0},
{"Ura Foxe", 1300.0},
{"Iby Stout", 1800.0}
};
double * pd[3];
// 指向结构中的 amount
for (int i = 0; i < 3; i++)
pd[i] = &mr_E[i].amount;
cout << "Listing Mr. E's counts of things:\n";
ShowArray(things, 6); // 使用模板A
cout << "Listing Mr. E's debts:\n";
ShowArray(pd, 3); // 使用模板 B
return 0;
}
template <typename T>
void ShowArray(T arr[], int n)
{
using namespace std;
cout << "template A\n";
for (int i = 0; i < n; i++)
cout << arr[i] << ' ';
cout << endl;
}
template <typename T>
void ShowArray(T * arr[], int n)
{
using namespace std;
cout << "template B\n";
for (int i = 0; i < n; i++)
cout << *arr[i] << ' ';
cout << endl;
}
在上面代码中,对于 ShowArray(things, 6); ,things 是一个 int 型数组,因此与模板 A 匹配,模板中的 T 被替换为 int 。
对于 ShowArray(pd, 3); ,其中 pd 是一个 double * 数组,与模板 A 也匹配,模板中的 T 被替换为 double * 。在这种情况下,函数模板将显示 pd 数组的内容,即 3 个地址。但是,该函数的调用也与模板 B 匹配,此时 T 被替换为 double 类型,而函数将显示被解除引用的元素 *arr,即结构中 double 的值。
在这两个模板中,模板 B 更具体,因为它做了特定的假设 --- 数组内容是指针,因此被使用。从程序的输出也可以看到这一点:
如果在程序中去掉模板 B,那么编译器就会用模板 A 来匹配,这样显示的就是地址了,而不是值。
总结如下:
重载解析将寻找最匹配的函数。如果只存在一个这样的函数,则选择它。如果存在多个这样的函数,但其中只有一个是非模板函数,则选择该函数。如果存在多个适合的函数,且它们都为模板函数,但其中有一个函数比其他函数更具体,则选择该函数。如果有多个同样合适的非模板函数或模板函数,但没有一个函数比其他函数更具体,则函数调用是不确定的,因此是错误的。当然,如果不存在匹配的函数,那么也是错误的。
自己选择
除了让编译器选择最合适的函数外,也允许你自己做出选择,尽管你做出的选择和编译器的选择不一样。看下面的程序:
[C++] 纯文本查看 复制代码 // choices.cpp -- choosing a template
#include <iostream>
template<class T>
T lesser(T a, T b) // #1
{
return a < b ? a : b;
}
int lesser(int a, int b) // #2
{
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;
cout << lesser(m, n) << endl; // use #2
cout << lesser(x, y) << endl; // use #1 with double
cout << lesser<>(m, n) << endl; // use #1 with int
cout << lesser<int>(x, y) << endl; // use #1 with int
// cin.get();
return 0;
}
输出结果:
最后的函数调用将 double 转换为 int,有些编译器会对此发出警告。
上面程序中,提供了一个模板和一个标准函数,其中模板返回两个值当中较小的一个,而标准函数返回两个值中绝对值较小的那个。
对于 cout << lesser(m, n) << endl; 这条语与函数模板和非函数模板都匹配,因此编译器选择了非函数模板,并返回 20 。
接着,对于 cout << lesser(x, y) << endl; ,它与模板相匹配(T 为 double),因此返回 15.5 。
接着,对于 cout << lesser<>(m, n) << endl; ,lesser<>(m, n) 中的 <> 指出,编译器应该选择模板函数,而不是非模板函数;编译器注意到实参的类型为 int,因此使用 int 替代 T 对模板进行实例化。
最后,对于 cout << lesser<int>(x, y) << endl; ,这条语句要求进行显式实例化(使用 int 替代 T),将使用显式实例化得到函数。x 和 y 的值将被强制转换为 int,该函数返回一个 int 值,这就是最后输出 15 而不是 15.5 的原因所在。 |