重载与模板
函数模板可以被另一个模板或一个普通非模板函数重载。
与往常一样,名字相同的函数必须具有不同数量或类型的参数。
如果涉及函数模板,则函数匹配规则会在以下几方面受到影响:
- 对于一个调用,其候选函数包括所有模板实参推断成功的函数模板实例。
- 候选的函数模板总是可行的,因为模板实参推断会排除任何不可行的模板。
- 与往常一样,可行函数(模板与非模板)按类型转换(如果对此调用需要的话)来排序。当然,可以用于函数模板调用的类型转换是非常有限的。
- 与往常一样,如果恰有一个函数提供比任何其他函数都更好的匹配,则选择此函数。但是,如果有多个函数提供同样好的匹配,则:
——如果同样好的函数中只有一个是非模板函数,则选择此函数,
——如果同样好的函数中没有非模板函数,而有多个函数模板,且其中一个模板比其他 模板更特例化,则选择此模板。
——否则,此调用有歧义。
正确定义一组重载的函数模板需要对类型间的关系及模板函数允许的有限的实参类型转换有深刻的理解。
编写重载模板
作为一个例子,我们将构造一组函数,它们在调试中可能很有用。
我们将这些调试函数命名为debug_rep,每个函数都返回一个给定对象的string表示。
我们首先编写此函数的最通用版本,将它定义为一个模板,接受一个const对象的引用:
//打印任何我们不能处理的类型
template <typename T>
string debug_rep(const T& t)
{
ostringstream ret;
ret << t; // 使用T的输出运算符打印t的一个表示形式
return ret.str();// 返回ret 绑定的string的一个副本
}
此函数可以用来生成一个对象对应的string表示,该对象可以是任意具备输出运算符的类型。
接下来,我们将定义打印指针的debug_rep版本:
template <typename T>
string debug_rep(T* p)
{
cout << "使用了T*p版本" << endl;
ostringstream ret; // 打印指针本身的值
ret << "pointer: " << p;
if (p)
ret << "" << debug_rep(*p);// 打印p指向的值
else
ret << " null pointer"; // 或指出ρ为空
return ret.str();// 返回ret 绑定的string的一个副本
}
此版本生成一个string,包含指针本身的值和调用debug_rep获得的指针指向的值。
注意此函数不能用于打印字符指针,因为IO库为char+值定义了一个<<版本。此<版本假定指针表示一个空字符结尾的字符数组,并打印数组的内容而非地址值。
我们可以这样使用这些函数:
#include<iostream>
#include<sstream>
using namespace std;
//打印任何我们不能处理的类型
template <typename T>
string debug_rep(const T& t)
{
cout << "使用了const T&t版本" << endl;
ostringstream ret;
ret << t; // 使用T的输出运算符打印t的一个表示形式
return ret.str();// 返回ret 绑定的string的一个副本
}
template <typename T>
string debug_rep(T* p)
{
cout << "使用了T*p版本" << endl;
ostringstream ret; // 打印指针本身的值
ret << "pointer: " << p;
if (p)
ret << "" << debug_rep(*p);// 打印p指向的值
else
ret << " null pointer"; // 或指出ρ为空
return ret.str();// 返回ret 绑定的string的一个副本
}
int main()
{
string s("hi");
cout << debug_rep(s)<<endl;
}
对于这个调用,只有第一个版本的debug_rep是可行的。
第二个debug_rep版本要求一个指针参数,但在此调用中我们传递的是一个非指针对象。
因此编译器无法从一个非指针实参实例化一个期望指针类型参数的函数模板,因此实参推断失败。
由于只有一个可行函数,所以此函数被调用。
如果我们用一个指针调用 debug_rep:
cout << debug_rep(&s)<<endl;
两个函数都生成可行的实例:
- debug _rep (const string*&),由第一个版本的debug_rep实例化而来,被绑定到string*。
- debug_rep(string*),由第二个版本的debug_rep实例化而来,T被绑定到string。
第二个版本的debug rep的实例是此调用的精确匹配。
第一个版本的实例需要进行普通指针到 const指针的转换。
正常函数匹配规则告诉我们应该选择第二个模板,实际上编译器确实选择了这个版本。
#include<iostream>
#include<sstream>
using namespace std;
//打印任何我们不能处理的类型
template <typename T>
string debug_rep(const T& t)
{
cout << "使用了const T&t版本" << endl;
ostringstream ret;
ret << t; // 使用T的输出运算符打印t的一个表示形式
return ret.str();// 返回ret 绑定的string的一个副本
}
template <typename T>
string debug_rep(T* p)
{
cout << "使用了T*p版本" << endl;
ostringstream ret; // 打印指针本身的值
ret << "pointer: " << p;
if (p)
ret << "" << debug_rep(*p);// 打印p指向的值
else
ret << " null pointer"; // 或指出ρ为空
return ret.str();// 返回ret 绑定的string的一个副本
}
int main()
{
string s("hi");
string result = debug_rep(&s); // 将函数返回值存储在result变量中
cout << result << endl; // 打印result变量,即debug_rep函数的返回值
}
我们打开调试面板,在执行函数调用语句时,直接跳进了第二个模板函数
有人就好奇了为什么会出现第二行的提示
这里的关键在于模板函数的实例化和递归调用。当您调用
debug_rep(&s)
时,由于传递了一个指针,所以编译器会选择第二个模板函数debug_rep(T* p)
进行实例化。在这个函数内部,当指针p
不为空时,会递归调用debug_rep(*p)
。递归调用
debug_rep(*p)
时,传递的是指针p
所指向的值,即字符串s
的一个引用。因此,编译器会选择第一个模板函数debug_rep(const T& t)
进行实例化,其中T
被推导为std::string
。在第一个模板函数内部,
cout
语句会首先执行,打印出 "使用了const T&t版本"。接着,函数会使用输出运算符<<
将t
(即字符串s
)插入到一个ostringstream
对象中,并最终返回这个对象转换成的字符串。因此,当您运行程序时,会看到 "使用了const T&t版本" 被打印出来,这是因为在递归调用中第一个模板函数被实例化并执行了。
多个可行模板
作为另外一个例子,考虑下面的调用:
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的指针的调用永远是有歧义的。
当有多个重载模板对一个调用提供同样好的匹配时,应选择最特例化的版本。
#include<iostream>
#include<sstream>
using namespace std;
//打印任何我们不能处理的类型
template <typename T>
string debug_rep(const T& t)
{
cout << "使用了const T&t版本" << endl;
ostringstream ret;
ret << t; // 使用T的输出运算符打印t的一个表示形式
return ret.str();// 返回ret 绑定的string的一个副本
}
template <typename T>
string debug_rep(T* p)
{
cout << "使用了T*p版本" << endl;
ostringstream ret; // 打印指针本身的值
ret << "pointer: " << p;
if (p)
ret << "" << debug_rep(*p);// 打印p指向的值
else
ret << " null pointer"; // 或指出ρ为空
return ret.str();// 返回ret 绑定的string的一个副本
}
int main()
{
string s("hi");
const string* sp = &s;
cout << debug_rep(sp) << endl;
}
事实证明,确实是调用了T*版本
非模板和模板重载
作为下一个例子,我们将定义一个普通非模板版本的debug_rep来打印双引号包围的string:
string debug_rep(const string& s)
{
return '"' + s + '"';
}
现在,当我们对一个string 调用debug_rep时:
string s("hi");
cout << debug_rep(s) << endl;
有两个同样好的可行函数:
- debug_rep<string>(const string&),第一个模板,T被绑定到string*。
- debug_rep(const string&),普通非模板函数。
在本例中,两个函数具有相同的参数列表,因此显然两者提供同样好的匹配。但是,编译委会选择非模板版本。
当存在多个同样好的函数模板时,编译器选择最特例化的版本,出于相同的原因, 一个非模板函数比一个函数模板更好。
#include<iostream>
#include<sstream>
using namespace std;
//打印任何我们不能处理的类型
template <typename T>
string debug_rep(const T& t)
{
cout << "使用了const T&t版本" << endl;
ostringstream ret;
ret << t; // 使用T的输出运算符打印t的一个表示形式
return ret.str();// 返回ret 绑定的string的一个副本
}
template <typename T>
string debug_rep(T* p)
{
cout << "使用了T*p版本" << endl;
ostringstream ret; // 打印指针本身的值
ret << "pointer: " << p;
if (p)
ret << "" << debug_rep(*p);// 打印p指向的值
else
ret << " null pointer"; // 或指出ρ为空
return ret.str();// 返回ret 绑定的string的一个副本
}
//打印双引号包围的string
string debug_rep(const string& s)
{
return '"' + s + '"';
}
int main()
{
string s("hi");
cout << debug_rep(s) << endl;
}
对于一个调用,如果一个非函数模板与一个函数模板提供同样好的匹配,则选择非模板版本。
重载模板和类型转换
还有一种情况我们到目前为止尚未讨论:C风格字符串指针和字符串字面常量。
现在有了一个接受string的debug_rep版本,我们可能期望一个传递字符串的调用会匹配这个版本。但是,考虑这个调用:
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*版本更加特例化,编译器会选择它。
#include<iostream>
#include<sstream>
using namespace std;
//打印任何我们不能处理的类型
template <typename T>
string debug_rep(const T& t)
{
cout << "使用了const T&t版本" << endl;
ostringstream ret;
ret << t; // 使用T的输出运算符打印t的一个表示形式
return ret.str();// 返回ret 绑定的string的一个副本
}
template <typename T>
string debug_rep(T* p)
{
cout << "使用了T*p版本" << endl;
ostringstream ret; // 打印指针本身的值
ret << "pointer: " << p;
if (p)
ret << "" << debug_rep(*p);// 打印p指向的值
else
ret << " null pointer"; // 或指出ρ为空
return ret.str();// 返回ret 绑定的string的一个副本
}
//打印双引号包围的string
string debug_rep(const string& s)
{
return '"' + s + '"';
}
int main()
{
cout << debug_rep("hi world!") << endl; // 调用debug_rep(T*)
}
如果我们希望将字符指针按string处理,可以定义另外两个非模板重载版本:
//将字符指针转换为string,并调用string版本的debug_reg
string debug_rep(char* p)
{
return debug_rep(string(p));
}
string debug_rep(const char* p)
{
return debug_rep(string(p));
}
缺少声明可能导致程序行为异常
值得注意的是,为了使 char*版本的 debug_rep 正确工作,在定义此版本时,debug_rep (const string)的声明必须在作用域中。否则,就可能调用错误的debug_rep版本:
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));
}
通常,如果使用了一个忘记声明的函数,代码将编译失败。
但对于重载函数模板的函数而言,则不是这样。
如果编译器可以从模板实例化出与调用匹配的版本,则缺少的声明就不重要了。
在本例中,如果忘记了声明接受string参数的debug_rep版本,编译器会默默地实例化接受const T&的模板版本。
#include<iostream>
#include<sstream>
using namespace std;
//打印任何我们不能处理的类型
template <typename T>
string debug_rep(const T& t)
{
cout << "使用了const T&t版本" << endl;
ostringstream ret;
ret << t; // 使用T的输出运算符打印t的一个表示形式
return ret.str();// 返回ret 绑定的string的一个副本
}
template <typename T>
string debug_rep(T* p)
{
cout << "使用了T*p版本" << endl;
ostringstream ret; // 打印指针本身的值
ret << "pointer: " << p;
if (p)
ret << "" << debug_rep(*p);// 打印p指向的值
else
ret << " null pointer"; // 或指出ρ为空
return ret.str();// 返回ret 绑定的string的一个副本
}
string debug_rep(char* p)
{
// 如果接受一个const string&的版本的声明不在作用域中,
// 返回语句将调用 debug_rep(const T&)的T实例化为string的版本
return debug_rep(string(p));
}
int main()
{
cout << debug_rep("hi world!") << endl; // 调用debug_rep(T*)
}
在定义任何函数之前,记得声明所有重载的函数版本。这样就不必担心编译器T由于未遇到你希望调用的函数而实例化一个并非你所需的版本。