【C++基础】第九十五课:[模板与泛型编程]重载与模板

重载与模板

Posted by x-jeff on April 18, 2024

【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的一个副本
}

ostringstream用法

此函数可以用来生成一个对象对应的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));
}