C++ Template
?为什么要使用模板
不用模板方式实现功能
- 重复实现相同功能:针对每个所需相同行为的不同类型,一次又一次地实现它,做了许多重复的工作,会犯同一个错误;还会舍弃复杂但更好用的算法:因为复杂算法通常都趋向于引入更多的错误。
- 继承基类:把通用代码放在一个诸如Object或者void*的公共基础类里面。借助公共基类来编写通用代码,将失去类型检查这个优点。另外,对于以后实现的许多类,都必须继承自某个特定的基类,这会令代码的维护更加困难。
- 使用特殊的预处理程序:使用了一个诸如C或C++预处理器的预处理程序,那么将会失去源代码具有很好的格式这个优点,必须使用一些愚蠢的文本替换机制来替换源代码,而这将不会考虑作用域和类型。
函数模板
定义与使用模板
定义函数模板
返回两个值中最大者的函数模板,模板定义在头文件:
template <typename T>
inline T const& max(T const& a, T const& b) {
return a < b ? b : a;
}
??注意:typename与class定义模板参数等价,为避免歧义,最好用typename
使用函数模板
#include <iostream>
#include <string>
#include "max.h"
int main()
{
int i = 42;
std::cout << ::max(7, i) << std::endl;
std::string s1 = "ma";
std::string s2 = "m";
std::cout << ::max(s1, s2) << std::endl;
}
- max()模板每次调用的前面都有域限定符::,这是为了确认调用的是全局名字空间中的max()。因为标准库也有一个std:max()模板。
- 通常而言,并不是把模板编译成一个可以处理任何类型的单一实体,而是对于实例化模板参数的每种类型,都从模板产生出一个不同的实体。
如果试图基于一个不支持模板内部所使用操作的类型实例化一个模板,那么将会导致一个编译期错误,可以得出一个结论:模板被编译了两次,分别发生在:
-
实例化之前,先检查模板代码本身,查看语法是否正确;在这里会发现错误的语法,如遗漏分号等。 -
在实例化期间,检查模板代码,查看是否所有的调用都有效。在这里会发现无效的调用,如该实例化类型不支持某些函数调用等,
实参推导(deduction)
调用一个模板时,模板参数可以由所传递的实参来决定。如果传递了两个int给参数类型T cons&,那么C++编译器能够得出结论:T必须是int。注意,这里不允许进行自动类型转换,每个T都必须正确地匹配。以max()举个🌰:
max(4, 7);
max(4, 4.2);
有3种方法可以用来处理上面这个错误:
- 对实参进行强制类型转换,使它们可以互相匹配
- 显式指定(或者限定)T的类型
- 指定两个参数可以具有不同的类型
模板参数
函数模板有两种类型的参数:
template <typename T>
...max(T const& a, T const& b);
- 模板参数:位于函数模板名称的前面,在一对尖括号内部进行声明
- 调用参数:位于函数模板名称之后,在一对圆括号内部进行声明
可以根据需要声明任意数量的模板参数,可以定义调用参数的类型不同的模板:
template <typename T1, typename T2>
inline T1 max(T1 const& a, T2 const& b);
max(4, 4.2);
这看起来是一种能够给模板传递两个不同类型调用参数的好方法,但在这个例子中,这种方法是有缺点的。主要问题是:
-
必须声明返回类型,对于返回类型,如果使用的是其中的一个参数类型,那么另一个参数的实参就可能要自动转型为返回类型,而不会在意调用者的意图。于是,取决于调用实参的顺序,42和66.66的最大值可以是浮点数66.66,也可以是整数66。 -
另一个缺点是:把第2个参数转型为返回类型的过程将会创建一个新的局部临时对象,这导致了不能通过引用来返回结果(作用域在函数内部,临时对象在程序离开作用域后销毁,返回引用指向无效内存)。因此,在我们的例子里,返回类型必须是T1,而不能是T1 const&。
因为调用参数的类型构造自模板参数,所以模板参数和调用参数通常是相关的。我们把这个概念称为:函数模板的实参推导。可以像调用普通函数那样调用函数模板。
当模板参数和调用参数没有发生关联,或者不能由调用参数来决定模板参数的时候,在调用时就必须显式指定模板实参。例如,可以引入第3个模板实参类型,来定义函数模板的返回类型:
template <typename T1, typename T2, typename RT>
inline RT max(T1 const& a, T2 const& b);
RT不会出现在函数调用参数的类型里面,无法自动推导RT类型,调用时必须显式指定实参
max<int, double, double> (4, 4.2);
目前为止,只是考察显式指定所有函数模板实参的例子,和不显式指定函数任何模板实参的例子。另一种情况是只显式指定第一个实参,而让推导过程推导出其余的实参。通常而言,必须指定最后一个不能被隐式推导的模板实参之前所有实参类型。因此,在上面的例子里,如果改变模板参数的声明顺序,那么调用者就只需要指定返回类型:
template <typename RT, typename T1, typename T2>
inline RT max(T1 const& a, T2 const& b);
max<double> (4, 4.2);
重载函数模板
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;
}
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);
::max(7.0, 42.0);
::max('a', 'b');
::max(7, 42);
::max<>(7, 42);
::max<double>(7, 42);
::max('a', 42);
}
一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为这个非模板函数。对于非模板函数和同名的函数模板,如果其他条件都是相同的话,那么在调用的时候,重载解析过程通常会调用非模板函数,而不会从该模板产生出一个实例。
??注意:函数的所有重载版本的声明都应该位于该函数被调用的位置之前。
小结
-
模板函数为不同的模板实参定义了一个函数家族。 -
当传递模板实参的时候,可以根据实参的类型来对函数模板进行实例化。 -
可以显式指定模板参数。 -
可以重载函数模板。 -
当重载函数模板的时候,改变限制在:显式地指定模板参数。 -
一定要让函数模板的所有重载版本的声明都位于它们被调用的位置之前。
类模板
😮?💨 未完待续…
模板特例化
参考资料
《C++ Template》
|