【C++基础】系列博客为参考《C++ Primer中文版(第5版)》(C++11标准)一书,自己所做的读书笔记。
本文为原创文章,未经本人允许,禁止转载。转载请注明出处。
1.重载与模板
函数模板可以被另一个模板或一个普通非模板函数重载。与往常一样,名字相同的函数必须具有不同数量或类型的参数。
如果涉及函数模板,则函数匹配规则(参见:函数重载)会在以下几方面受到影响:
- 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例。
- 候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板。
- 与往常一样,可行函数(模板与非模板)按类型转换(如果对此调用需要的话)来排序。当然,可以用于函数模板调用的类型转换是非常有限的(参见:类型转换与模板类型参)。
- 与往常一样,如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数。但是,如果有多个函数提供同样好的匹配,则:
- 如果同样好的函数中只有一个是非模板函数,则选择此函数。
- 如果同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他模板更特例化,则选择此模板。
- 否则,此调用有歧义。
1.1.编写重载模板
作为一个例子,我们将构造一组函数,它们在调试中可能很有用。我们将这些调试函数命名为debug_rep,每个函数都返回一个给定对象的string表示。我们首先编写此函数的最通用版本,将它定义为一个模板,接受一个const对象的引用:
1
2
3
4
5
6
7
//打印任何我们不能处理的类型
template <typename T> string debug_rep(const T &t)
{
ostringstream ret;
ret << t; //使用T的输出运算符打印t的一个表示形式
return ret.str(); //返回ret绑定的string的一个副本
}
此函数可以用来生成一个对象对应的string表示,该对象可以是任意具备输出运算符的类型。
接下来,我们将定义打印指针的debug_rep版本:
1
2
3
4
5
6
7
8
9
10
11
12
//打印指针的值,后跟指针指向的对象
//注意:此函数不能用于char*
template <typename T> string debug_rep(T *p)
{
ostringstream ret;
ret << "pointer: " << p; //打印指针本身的值
if(p)
ret << " " << debug_rep(*p); //打印p指向的值
else
ret << " null pointer"; //或指出p为空
return ret.str(); //返回ret绑定的string的一个副本
}
注意此函数不能用于打印字符指针,因为IO库为char*
值定义了一个<<
版本。此<<
版本假定指针表示一个空字符结尾的字符数组,并打印数组的内容而非地址值。
我们可以这样使用这些函数:
1
2
string s("hi");
cout << debug_rep(s) << endl;
对于这个调用,只有第一个版本的debug_rep是可行的。如果我们用一个指针调用debug_rep:
1
cout << debug_rep(&s) << endl;
两个函数都生成可行的实例:
debug_rep(const string*&)
,由第一个版本的debug_rep实例化而来,T被绑定到string*
。debug_rep(string*)
,由第二个版本的debug_rep实例化而来,T被绑定到string。
第二个版本的debug_rep的实例是此调用的精确匹配。第一个版本的实例需要进行普通指针到const指针的转换。正常函数匹配规则告诉我们应该选择第二个模板,实际上编译器确实选择了这个版本。
1.2.多个可行模板
作为另外一个例子,考虑下面的调用:
1
2
const string *sp = &s;
cout << debug_rep(sp) << endl;
此例中的两个模板都是可行的,而且两个都是精确匹配:
debug_rep(const string*&)
,由第一个版本的debug_rep实例化而来,T绑定到string*
。debug_rep(const string*)
,由第二个版本的debug_rep实例化而来,T被绑定到const string
。
在此情况下,正常函数匹配规则无法区分这两个函数。我们可能觉得这个调用将是有歧义的。但是,根据重载函数模板的特殊规则,此调用被解析为debug_rep(T*)
,即,更特例化的版本。
设计这条规则的原因是,没有它,将无法对一个const的指针调用指针版本的debug_rep。问题在于模板debug_rep(const T&)
本质上可以用于任何类型,包括指针类型。此模板比debug_rep(T*)
更通用,后者只能用于指针类型。没有这条规则,传递const的指针的调用永远是有歧义的。
当有多个重载模板对一个调用提供同样好的匹配时,应选择最特例化的版本。
1.3.非模板和模板重载
作为下一个例子,我们将定义一个普通非模板版本的debug_rep来打印双引号包围的string:
1
2
3
4
5
//打印双引号包围的string
string debug_rep(const string &s)
{
return '"' + s + '"';
}
现在,当我们对一个string调用debug_rep时:
1
2
string s("hi");
cout << debug_rep(s) << endl;
有两个同样好的可行函数:
debug_rep<string>(const string&)
,第一个模板,T被绑定到string*
。debug_rep(const string&)
,普通非模板函数。
在本例中,两个函数具有相同的参数列表,因此显然两者提供同样好的匹配。但是,编译器会选择非模板版本。当存在多个同样好的函数模板时,编译器选择最特例化的版本,出于相同的原因,一个非模板函数比一个函数模板更好。
对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。
1.4.重载模板和类型转换
还有一种情况我们到目前为止尚未讨论:C风格字符串指针和字符串字面常量。现在有了一个接受string的debug_rep版本,我们可能期望一个传递字符串的调用会匹配这个版本。但是,考虑这个调用:
1
cout << debug_rep("hi world!") << endl; //调用debug_rep(T*)
本例中所有三个debug_rep版本都是可行的:
debug_rep(const T&)
,T被绑定到char[10]
。debug_rep(T*)
,T被绑定到const char。debug_rep(const string&)
,要求从const char*
到string的类型转换。
对给定实参来说,两个模板都提供精确匹配——第二个模板需要进行一次(许可的)数组到指针的转换,而对于函数匹配来说,这种转换被认为是精确匹配。非模板版本是可行的,但需要进行一次用户定义的类型转换,因此它没有精确匹配那么好,所以两个模板成为可能调用的函数。与之前一样,T*
版本更加特例化,编译器会选择它。
如果我们希望将字符指针按string处理,可以定义另外两个非模板重载版本:
1
2
3
4
5
6
7
8
9
//将字符指针转换为string,并调用string版本的debug_rep
string debug_rep(char *p)
{
return debug_rep(string(p));
}
string debug_rep(const char *p)
{
return debug_rep(string(p));
}
1.5.缺少声明可能导致程序行为异常
值得注意的是,为了使char*
版本的debug_rep正确工作,在定义此版本时,debug_rep(const string&)
的声明必须在作用域中。否则,就可能调用错误的debug_rep版本:
1
2
3
4
5
6
7
8
9
10
template <typename T> string debug_rep(const T &t);
template <typename T> string debug_rep(T *p);
//为了使debug_rep(char*)的定义正确工作,下面的声明必须在作用域中
string debug_rep(const string &);
string debug_rep(char *p)
{
//如果接受一个const string&的版本的声明不在作用域中,
//返回语句将调用debug_rep(const T&)的T实例化为string的版本
return debug_rep(string(p));
}