写在前面
面向对象编程(OOP)和泛型编程都能处理在编写程序时不知道类型的情况。不同之处在于:OOP能处理类型在程序运行之前都未知的情况;而在泛型编程中,在编译时就能获知类型了。
模板是泛型编程的基础。我们不必了解模板是如何定义的就能使用它们,例我们用过的vector。
定义
模板就是实现代码重用机制的一种工具,它可以实现类型参数化,即把类型定义为参数, 从而实现了真正的代码可重用性。
模版可以分为两类,一个是函数模版,另外一个是类模版。
我们什么时候考虑使用模板技术呢?
假定我们希望编写一个函数来比较两个值,并指出第一个值是小于、等于还是大于第二个值。在实际中,我们可能想要定义多个函数,每个函数比较一种给定类型的值。例:尝试定义多个重载函数
//如果两个const string&类型的值相等,返回0,如果v1 小返回-1,如果v2小返回1
int compare(const string & v1, const string &v2)
{
if (v1 < v2) return -1;
else if (v2 < v1) return 1;
return 0;
}
//下面重载是是double类型的
int compare(const double& v1, const double& v2)
{
if (v1 < v2) return -1;
else if (v2 < v1) return 1;
return 0;
}
可以发现这两个函数几乎是相同的,唯一的差异是参数的类型,函数体则完全一样。
因此当发现有上述情况时,就可以考虑使用函数模板了。
函数模板
首先看一下函数模板的定义:在定义函数时,函数的类型和函数的参数类型不具体指定,而用一个虚拟的类型来代替,实际上是建立一个通用的函数,称这样的函数为函数模板。
函数模板以关键字template开始,后跟一个模板参数列表(template parameter list),这是一个以逗号分隔的一个或多个**模板参数(template parameter)**的列表,用<> 包围起来。例:
template <typename T>
int compare(const T& v1, const T& v2)
{
if (v1 < v2) return -1;
else if (v2 < v1) return 1;
return 0;
}
注意:在模板定义中,模板参数列表不能为空。
//错误的定义
template <>
void func()
{
//一些实现
}
模板参数列表的作用很想函数参数列表。函数参数列表定义了若干特定类型的局部变量,但并未指出如何初始化它们。在运行时调用者提供实参来初始化形参。
类似的,模板参数表示在类或函数定义中用到的类型或值,当使用模板时,我们(隐式的或显式地)指定模板实参(template argument),将其绑定到模板参数上。
例上面的compare函数声明了一个名为T的类型参数。在compare中,我们用名字T表示一个类型。而T表示的实际类型则在编译时根据compare的使用情况来确定。
实例化函数模板
当我们调用一个函数模板时,编译器通常用函数实参来为我们推断模板实现。例:
cout << compare(1, 0) << endl;
//通过compare的函数实参1, 0推断处T 为int,并将其绑定到模板参数T。
编译器用推断出的模板参数来为我们**实例化(instantiate)**一个特定版本的函数。例:
//实例化出int compare(const int& , const int&)
cout << compare(1, 0) << endl; //T 为 int
//实例化出int compare(const vector<int>&, const vector<int>&)
vector<int> v1{1, 2, 3, 4}, v2{5, 6, 7};
cout << compare(v1, v2) << endl; //T 为 vector<int>
编译器会实例化出两个不同版本的compare。对于第一个调用【compare(1, 0)】,编译器会编写并编译一个compare版本,其中T被替换为int。如下:
int compare(const int& v1, const int& v2)
{
if (v1 < v2) return -1;
else if (v2 < v2) return 1;
return 0;
}
同理,对于第二个调用【compare(v1, v2)】,编译器会生成另一个compare版本,其中T被替换为vector。
这些编译器生成的版本通常被称为模板 的实例(instantiation)。
模板类型参数
我们的compare函数有一个模板类型参数(type parameter)(即上面的T)。类型参数前必须使用关键字class 或 typename。
在模板参数列表中,这两个关键字的含义相同,可以互换使用。一个模板参数列表张可以同时使用这两个关键字。例:
//正确用法
template <typename T, class U>
void calc(const T&, const U&);
//错误用法:类型参数列表中的U没有加关键字
template<typename T, U>
void calc(const T&, const U&);
非类型模板参数
除了定义类型参数,还可以在模板中定义非类型参数(nontype parameter)。
一个非类型参数表示一个值而非一个类型,通过一个特定的类型名(而非关键字class 或 typename)来指定非类型参数。
当一个模板被实例化(即调用函数模板)时,非类型参数被一个用户提供的或编译器推断出的值所代替,这些值必须是常量表达式(constexpr),从而允许编译器在编译时实例化模板。
例:
//处理字符串字面常量的模板
//我们希望能比较不同长度的字符串字面常量,因此模板定义了两个非类型参数:第一个模板参数表示第一个数组的长度,第二个参数表示第二个数组的长度
template<unsigned N, unsigned M>
int compare(const char (&p1)[N], const char (&p2)[M])
{
return strcmp(p1, p2);
}
//当我们调用这个版本的模板时:compare("hi", "mom");
//编译器会根据实参推断出N = 3,M = 4。
//为什么不是2和3呢? 因为我们的实参是string类型的,string转char* 会在末尾插入一个空字符('\0')作为终止符。因此编译器会实例化出如下版本:int compare(const char (&p1)[3], const char (&p2)[4]);
一个非类型参数可以是一个整型,也可以是一个指向对象或函数的指针或引用。
需要注意的是绑定到非类型整型参数的实参必须是一个常量表达式。绑定到指针或引用非类型参数的实参必须具有静态的生存期。例:
int a = 3;
int b = 4;
//调用处理字符串字面常量的模板
//错误调用
compare<a, b>("hi", "mom"); //这里显示指定模板实参为3, 4。但这里是一个错误的调用。因为变量a和变量b不是常量表达式
//下面为正确调用
const unsigned int c = 3; //c是常量表达式
const unsigned int d = 4; //d也是常量表达式
compare<c, d>("hi", "mom"); //正确调用
inline 和 constexpr的函数模板
函数模板可以声明为inline 或 constexpr的,如同非模板函数一样。inline 或 constexpr说明符放在模板参数列表之后,返回类型之前:
template<typename T>
inline T min(const T&, const T&);
template<typename T>
constexpr T min(const T&, const T&);
编写类型无关的代码
我们最初的compare函数虽然简单,但它说明了编写泛型代码的两个重要原则:
- 模板中的函数参数是const的引用。
- 函数体中的条件判断仅使用<比较运算。
将函数参数设定为const&,保证了函数可以用于不能拷贝的类型(例拷贝构造函数和赋值拷贝构造被删除的类型),且使得compare在处理大对象是使函数允许的更快。
为什么只使用<运算符而不是< 和 > 一起使用?
因为只使用<运算符,有效的降低了compare函数对要处理的类型的要求:这些类型必须支持<, 但不必同时支持>。例某个类型重载了<运算符而没有重载>运算符。
所以,模板程序应该尽量减少对实参类型的要求。
模板编译
当编译器遇到一个模板定义时,它并不生成代码。只有当我们实例化出一个模板的一个特定版本时,编译器才会生成代码。参考上例:
//编译到如下定义时,并不生成代码
template <typename T>
int compare(const T& v1, const T& v2)
{
if (v1 < v2) return -1;
else if (v2 < v1) return 1;
return 0;
}
//编译到如下语句时,通过模板实参(1, 0)推断出模板类型T 为int,然后生成该版本的实例
cout << compare(1, 0) << endl; //T 为 int
为了生成一个实例化版本,编译器需要掌握函数模板或类模板成员函数的定义,因此模板的头文件中通常既包括声明也包括实现。
总结
当调用一个函数模板时,编译器会根据给定的函数实参来推断模板实参,并以此实际类型参数代替模板参数来创建出模板的一个新的实例,也就是一个真正可以调用的函数。