当前位置:嗨网首页>书籍在线阅读

20-编译器选择使用哪个函数版本

  
选择背景色: 黄橙 洋红 淡粉 水蓝 草绿 白色 选择字体: 宋体 黑体 微软雅黑 楷体 选择字体大小: 恢复默认

8.5.5 编译器选择使用哪个函数版本

对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时。这个过程称为重载解析(overloading resolution)。详细解释这个策略将需要将近一章的篇幅,因此我们先大致了解一下这个过程是如何进行的。

  • 第1步:创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
  • 第2步:使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。例如,使用float参数的函数调用可以将该参数转换为double,从而与double形参匹配,而模板可以为float生成一个实例。
  • 第3步:确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。

考虑只有一个函数参数的情况,如下面的调用:

may('B'); // actual argument is type char

首先,编译器将寻找候选者,即名称为may()的函数和函数模板。然后寻找那些可以用一个参数调用的函数。例如,下面的函数符合要求,因为其名称与被调用的函数相同,且可只给它们传递一个参数:

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)不可行,因为整数类型不能被隐式地转换(即没有显式强制类型转换)为指针类型。剩余的一个模板可用来生成具体化,其中T被替换为char类型。这样剩下5个可行的函数,其中的每一个函数,如果它是声明的唯一一个函数,都可以被使用。

接下来,编译器必须确定哪个可行函数是最佳的。它查看为使函数调用参数与可行的候选函数的参数匹配所需要进行的转换。通常,从最佳到最差的顺序如下所述。

1.完全匹配,但常规函数优先于模板。

2.提升转换(例如,char和shorts自动转换为int,float自动转换为double)。

3.标准转换(例如,int转换为char,long转换为double)。

4.用户定义的转换,如类声明中定义的转换。

例如,函数#1优于函数#2,因为char到int的转换是提升转换(参见第3章),而char到float的转换是标准转换(参见第3章)。函数#3、函数#5和函数#6都优于函数#1和#2,因为它们都是完全匹配的。#3和#5优于#6,因为#6函数是模板。这种分析引出了两个问题。什么是完全匹配?如果两个函数(如#3和#5)都完全匹配,将如何办呢?通常,有两个函数完全匹配是一种错误,但这一规则有两个例外。显然,我们需要对这一点做更深入的探讨。

1.完全匹配和最佳匹配

进行完全匹配时,C++允许某些“无关紧要的转换”。表8.1列出了这些转换—— Type表示任意类型。例如,int实参与int &形参完全匹配。注意,Type可以是char &这样的类型,因此这些规则包括从char &到const char &的转换。Type(argument-list)意味着用作实参的函数名与用作形参的函数指针只要返回类型和参数列表相同,就是匹配的(第7章介绍了函数指针以及为何可以将函数名作为参数传递给接受函数指针的函数)。第9章将介绍关键字volatile。

表8.1 完全匹配允许的无关紧要转换

| 从 实 参 | 到 形 参 | | :----- | :----- | :----- | :----- | | 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, "spots"};
...
recycle(ink);

在这种情况下,下面的原型都是完全匹配的:

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,则将出现二义性错误。

一个完全匹配优于另一个的另一种情况是,其中一个是非模板函数,而另一个不是。在这种情况下,非模板函数将优先于模板函数(包括显式具体化)。

如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先。例如,这意味着显式具体化将优于使用模板隐式生成的具体化:

struct blot {int a; char b[10];};
template <class Type> void recycle (Type t); // template
template <> void recycle<blot> (blot & t);   // specialization for blot
...
blot ink = {25, "spots"};
...
recycle(ink); // use specialization

术语“最具体(most specialized)”并不一定意味着显式具体化,而是指编译器推断使用哪种类型时执行的转换最少。例如,请看下面两个模板:

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); // address of a structure

recycle(&ink)调用与#1模板匹配,匹配时将Type解释为blot 。recycle(&ink)函数调用也与#2模板匹配,这次Type被解释为ink。因此将两个隐式实例——recycle<blot >(blot )和recycle (blot )发送到可行函数池中。

在这两个模板函数中,recycle<blot >(blot )被认为是更具体的,因为在生成过程中,它需要进行的转换更少。也就是说,#2模板已经显式指出,函数参数是指向Type的指针,因此可以直接用blot标识Type;而#1模板将Type作为函数参数,因此Type必须被解释为指向blot的指针。也就是说,在#2模板中,Type已经被具体化为指针,因此说它“更具体”。

用于找出最具体的模板的规则被称为函数模板的部分排序规则(partial ordering rules)。和显式实例一样,这也是C++98新增的特性。

2.部分排序规则示例

我们先看一个完整的程序,它使用部分排序规则来确定要使用哪个模板定义。程序清单8.14有两个用来显示数组内容的模板定义。第一个定义(模板A)假设作为参数传递的数组中包含了要显示的数据;第二个定义(模板B)假设数组元素为指针,指向要显示的数据。

程序清单8.14 temptempover.cpp

// tempover.cpp -- template overloading
#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];
// set pointers to the amount members of the structures in mr_E
    for (int i = 0; i < 3; i++)
        pd[i] = &mr_E[i].amount;
    cout << "Listing Mr. E's counts of things:\n";
// things is an array of int
    ShowArray(things, 6); // uses template A
    cout << "Listing Mr. E's debts:\n";
// pd is an array of pointers to double
    ShowArray(pd, 3); // uses template B (more specialized)
    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数组的名称,因此与下面的模板匹配:

template <typename T> // template A
void ShowArray(T arr[], int n);

其中T被替换为int类型。

接下来,请看下面的函数调用:

ShowArray(pd, 3);

其中pd是一个double *数组的名称。这与模板A匹配:

template <typename T> // template A
void ShowArray(T arr[], int n);

其中,T被替换为类型double *。在这种情况下,模板函数将显示pd数组的内容,即3个地址。该函数调用也与模板B匹配:

template <typename T>         // template B
void ShowArray(T * arr[], int n);

在这里,T被替换为类型double,而函数将显示被解除引用的元素*arr[i],即数组内容指向的double值。在这两个模板中,模板B更具体,因为它做了特定的假设——数组内容是指针,因此被使用。

下面是程序清单8.14中程序的输出:

Listing Mr. E's counts of things:
template A
13 31 103 301 310 130
Listing Mr. E's debts:
template B
2400 1300 1800

如果将模板B从程序中删除,则编译器将使用模板A来显示pd的内容,因此显示的将是地址,而不是值。请试试看。

简而言之,重载解析将寻找最匹配的函数。如果只存在一个这样的函数,则选择它;如果存在多个这样的函数,但其中只有一个是非模板函数,则选择该函数;如果存在多个适合的函数,且它们都为模板函数,但其中有一个函数比其他函数更具体,则选择该函数。如果有多个同样合适的非模板函数或模板函数,但没有一个函数比其他函数更具体,则函数调用将是不确定的,因此是错误的;当然,如果不存在匹配的函数,则也是错误。

3.创建自定义选择

在有些情况下,可通过编写合适的函数调用,引导编译器做出您希望的选择。请看程序清单8.15,该程序将模板函数定义放在文件开头,从而无需提供模板原型。与常规函数一样,通过在使用函数前提供模板函数定义,它让它也充当原型。

程序清单8.15 choices.cpp

// choices.cpp -- choosing a template
#include <iostream>
template<class T>   // or template <typename 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
    return 0;
}

最后的函数调用将double转换为int,有些编译器会针对这一点发出警告。

该程序的输出如下:

20
15.5
-30
15

程序清单8.15提供了一个模板和一个标准函数,其中模板返回两个值中较小的一个,而标准函数返回两个值中绝对值较小的那个。如果函数定义是在使用函数前提供的,它将充当函数原型,因此这个示例无需提供原型。请看下面的语句:

cout << lesser(m, n) << endl; // use #2

这个函数调用与模板函数和非模板函数都匹配,因此选择非模板函数,返回20。

接下来,下述语句中的函数调用与模板匹配(T为double):

cout << lesser(x, y) << endl;  // use #1 with double

现在来看下面的语句:

cout << lesser<>(m, n) << endl; // use #1 with int

lesser<>(m, n)中的<>指出,编译器应选择模板函数,而不是非模板函数;编译器注意到实参的类型为int,因此使用int替代T对模板进行实例化。

最后,请看下面的语句:

cout << lesser<int>(x, y) << endl; // use #1 with int

这条语句要求进行显式实例化(使用int替代T),将使用显式实例化得到的函数。x和y的值将被强制转换为int,该函数返回一个int值,这就是程序显示15而不是15.5的原因所在。

4.多个参数的函数

将有多个参数的函数调用与有多个参数的原型进行匹配时,情况将非常复杂。编译器必须考虑所有参数的匹配情况。如果找到比其他可行函数都合适的函数,则选择该函数。一个函数要比其他函数都合适,其所有参数的匹配程度都必须不比其他函数差,同时至少有一个参数的匹配程度比其他函数都高。

本书并不是要解释复杂示例的匹配过程,这些规则只是为了让任何一组函数原型和模板都存在确定的结果。