2 函数模板
函数模板是那些被参数化的函数, 它们代表的是一个函数家族。
2.1 初探函数模板
如下就是一个返回两个值中最大值的函数模板:
template <typename T>
inline T const& max(T const& a, T const& b)
{
// 如果a < b, 那么返回b, 否则返回a
return a < b ? b : a;
}
使用此模板:
int main()
{
int i = 42;
std::cout << "max(7, i) :" << ::max(7, i) << std::endl;
double f1 = 3.4;
double f2 = -6.7;
std::cout << "max(f1, f2) :" << ::max(f1, f2) << std::endl;
std::string s1 = "mathematics";
std::string s2 = "math";
std::cout << "max(s1, s2) :" << ::max(s1, s2) << std::endl;
}
max()模板每次调用的前面都有域限定符 :: ,这是为了确认我们调用的是全局名字空间中的max()。因为标准库也有一个
std::max()模板。
针对 3 种类型中的每一种,max()都被编译了一次。如果试图基于一个不支持模板内部所使用操作的类型实例化一个模
板, 那么将会导致一个编译期错误,例如:
std::complex<float> c1, c2; //std::complex并不支持 operator <
//…
max(c1, c2); //编译器错误
于是, 我们可以得出一个结论:
模板被编译了两次, 分别发生在:
1,实例化之前, 先检查模板代码本身, 查看语法是否正确; 在这里会发现错误的语法, 如遗漏分号等。
2,在实例化期间, 检查模板代码, 查看是否所有的调用都有效。 在这里会发现无效的调用, 如该实例化类型不支持某些函数调用等。
2.2 实参的演绎
我们为某些实参调用一个诸如 max()的模板时,模板参数可以由我们所传递的实参来决定。
如果我们传递了两个int给参数类型T const&, 那么C++编译器能够得出结论: T必须是int。 注意, 这里不允许进行自动类型转换; 每个T都必须正确地匹配。例如:
max(4, 7) //OK: 两个实参的类型都是int
max(4, 4.2) //ERROR:第1个T是int, 而第2个T是double
有3种方法可以用来处理上面这个错误:
1,对实参进行强制类型转换, 使它们可以互相匹配
max(static_cast<double>(4), 4.2) //OK
,2,显式指定(或者限定) T的类型:
max<double>(4, 4.2) //OK
3,指定两个参数可以具有不同的类型。(见2.3小节)
2.3 模板参数
函数模板有两种类型的参数。
1,模板参数: 位于函数模板名称的前面, 在一对尖括号内部进行声明:
template <typename T> //T是模板参数
2,调用参数:位于函数模板名称之后,在一对圆括号内部进行声明:
//…
max(T const& a, T const& b) //a和b都是调用参数
你可以根据需要声明任意数量的模板参数。然而,在函数模板内部(这一点和类模板有区别),不能指定缺省的模板实参。
template <typename T1, typename T2>
inline T1 max(T1 const& a, T2 const& b)
{
//...
}
max(4, 4.2) //OK 但第1个模板实参的类型定义了返回类型
这看起来是一种能够给 max()模板传递两个不同类型调用参数的好方法, 但在这个例子中, 这种方法是有缺点的:
1,必须声明返回类型
对于返回类型, 如果你使用的是其中的一个参数类型, 那么另一个参数的实参就可能要转型为返回类型, 而不会在意调用者的意图。
2,把第2个参数转型为返回类型的过程将会创建一个新的局部临时对象,这导致了你不能通过引用来返回结果。
因此, 在我们的例子里, 返回类型必须是T1, 而不能是T1 const&。
针对返回类型的问题,两个简单的解决方案是:
1,针对某些特定的类型,你还可以显式地实例化该模板:
max<double>(4, 4.2) //用double来实例化T 返回类型为double ok
2,引入第3个模板实参类型, 来定义函数模板的返回类型:
template <typename T1, typename T2, typename RT>
inline RT max (T1 const& a, T2 const& b);
然而, 模板实参演绎并不适合返回类型,因为RT不会出现在函数调用参数的类型里面。 因此,函数调用并不能演绎出RT。 于是, 你必须显式地指定模板实参列表。 例如:
template <typename T1, typename T2, typename RT>
inline RT max(T1 const& a, T2 const& b);
//…
max<int, double, double>(4, 4.2) //OK, 但是很麻烦
到目前为止, 我们只是考察了显式指定所有函数模板实参的例子,和不显式指定函数任何模板实参的例子。 另一种情况是只显式指定第一个实参, 而让演绎过程推导出其余的实参。 通常而言, 你必须指定“最后一个不能被隐式演绎的模板实参之前的”所有实参类型。 因此, 在上面的例子里, 如果你改变模板参数的声明顺序, 那么调用者就只需要指定返回类型:
template <typename RT, typename T1, typename T2>
inline RT max(T1 const& a, T2 const& b);
//…
max<double>(4, 4.2) //OK: 返回类型是double
在这个例子里, 调用max<double>时显式地把RT指定为double, 但其他两个参数T1和T2可以根据调用实参分别演绎为int和double。
2.4 重载函数模板
下面的简短程序叙述了如何重载一个函数模板:
//求两个int值的最大值
inline int const& max(int const& a, int const& b)
{
return a < b ? b : a;
}
//求两个任意类型值中的最大者
template <typename T>
inline T const& max(T const& a, T const& b)
{
return a < b ? b : a;
}
//求3个任意类型值中的最大者
template <typename T>
inline T const& max(T const& a, T const& b, T const& c)
{
return ::max(::max(a, b), c);
}
int main()
{
::max(7, 42, 68); // 调用具有3个参数的模板
::max(7.0, 42.0); // 调用max<double> (通过实参演绎)
::max('a', 'b'); // 调用max<char> (通过实参演绎)
::max(7, 42); // 调用int重载的非模板函数
::max<>(7, 42); // 调用 max<int> (通过实参演绎)
::max<double>(7, 42); //调用max<double> (没有实参演绎)
::max('a', 42.7); //调用int重载的非模板函数
}
因为模板是不允许自动类型转化的; 但普通函数可以进行自动类型转换, 所以最后一个调用将使用非模板函数(‘a’和42.7都被转化为int):
max('a',42.7) //对于不同类型的参数, 只允许使用非模板函数
下面这个更有用的例子将会为指针和普通的C字符串重载这个求最大值的模板:
// 求两个任意类型值的最大者
template <typename T>
inline T const& max(T const& a, T const& b)
{
return a < b ? b : a;
}
//求两个指针所指向值的最大者
template <typename T>
inline T* const& max(T* const& a, T* const& b)
{
return *a < *b ? b : a;
}
//求两个C字符串的最大者
inline char const* const& max(char const* const& a,
char const* const& b)
{
return std::strcmp(a, b) < 0 ? b : a;
}
int main()
{
int a = 7;
int b = 42;
::max(a, b); // max() 求两个int值的最大值
std::string s = "hey";
std::string t = "you";
::max(s, t); // max() 求两个std:string类型的最大值
int* p1 = &b;
int* p2 = &a;
::max(p1, p2); // max() 求两个指针所指向值的最大者
char const* s1 = "David";
char const* s2 = "Nico";
::max(s1, s2); // max() 求两个c字符串的最大值
}
一般而言, 在重载函数模板的时候, 最好只是改变那些需要改变的内容; 就是说, 你应该把你的改变限制在下面两种情况: 改变参数的数目或者显式地指定模板参数。
例如,对于原来使用传引用的max()模板, 你用C-string类型进行重载; 但对于现在(即重载版本的) 基于C-strings的max()函数, 你是通过传值来传递参数; 那么你就不能使用3个参数的max()版本, 来对3个C-string求最大值:
// 两个任意类型值的最大者 (通过传引用进行调用)
template <typename T>
inline T const& max(T const& a, T const& b)
{
return a < b ? b : a;
}
//两个C字符串的最大者(通过传值进行调用)
inline char const* max(char const* a, char const* b)
{
return std::strcmp(a, b) < 0 ? b : a;
}
//求3个任意类型值的最大者(通过传引用进行调用)
template <typename T>
inline T const& max(T const& a, T const& b, T const& c)
{
return max(max(a, b), c); //注意: 如果max(a,b)使用传值调用
//那么将会发生错误
}
int main()
{
::max(7, 42, 68); // OK
const char* s1 = "frederic";
const char* s2 = "anica";
const char* s3 = "lucas";
::max(s1, s2, s3); // 错误
}
问题在于: 如果你对3个C-strings调用max(), 那么语句:
return max (max(a,b),c);
将会产生一个错误。
这是因为对于C-strings而言, 这里的max(a,b)产生了一个新的临时局部值, 该值有可能会被外面的max函数以传引用
的方式返回, 而这将导致传回无效的引用。
2.5 小结
•模板函数为不同的模板实参定义了一个函数家族。
•当你传递模板实参的时候, 可以根据实参的类型来对函数模板进行实例化。
•你可以显式指定模板参数。
•你可以重载函数模板。
•当重载函数模板的时候, 把你的改变限制在: 显式地指定模板参数。
•一定要让函数模板的所有重载版本的声明都位于它们被调用的位置之前。