曲径通幽论坛

 找回密码
 立即注册
搜索
查看: 2846|回复: 0
打印 上一主题 下一主题

编译器如何选择合适的函数版本?

[复制链接]

716

主题

734

帖子

2946

积分

超级版主

Rank: 9Rank: 9Rank: 9

积分
2946
跳转到指定楼层
楼主
发表于 2013-12-23 09:55:42 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
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


如果像下面调用函数:
  1. may('B');          // 实参为 char 类型
复制代码
如果只考虑参数特征,而不考虑返回类型,首先排除 #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 *

假设有下面的代码:
  1. struct blot { int a; char b[10]; };
  2. blot ink = {25, "sports"};
  3. ...
  4. 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 。
一个完全匹配优于另一个的另一种情况是,其中一个是非模板函数,而另一个不是。在这种情况下,非模板参数将优先于模板参数(包括显式具体化)。

如果两个完全匹配的函数都是模板函数,则教具体的模板函数有限。这意味着,显式具体化将优先于使用模板隐式生成的具体化:
  1. struct blot {int a; char b[10];};
  2. template <class Type> void recycle (Type t); // 模板
  3. template <> void recycle<blot> (blot & t);   // 对 blot 具体化
  4. ...
  5. blot ink = {25, "spots"};
  6. ...
  7. recycle(ink);  // use 具体化
复制代码
术语 “最具体”(most specialized) 并不一定意味着显式具体化,而是指编译器推断使用哪种类型时执行的转换最少。例如,下面的两个模板:
  1. template <class Type> void rcycle (Type t);   #1
  2. template <class Type> void recycle (Type * t);   #1
复制代码
假设包含这些模板的程序也包含下面的代码:
  1. struct blot {int a; char b[10];};
  2. blot ink = {25, "spots"};
  3. ...
  4. 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;
}

输出结果:
20
15.5
-30
15

最后的函数调用将 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 的原因所在。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?立即注册

x
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|曲径通幽 ( 琼ICP备11001422号-1|公安备案:46900502000207 )

GMT+8, 2024-5-17 23:16 , Processed in 0.081243 second(s), 25 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表