假设我们希望编写一个函数来比较两个值,并指出第一个值是小于、等于还是大于第二个值。在实际中,我们可能就会想到定义多个重载函数:
//两个值相等返回0,第一个值小于第二个值返回-1,第一个值大于第二个值则返回1.
这两个函数几乎是相同的,唯一的差异是参数的类型。
如果对每种希望比较的类型都不得不重复定义完全一样的函数体,是非常繁琐且容易出错的。对于这样的问题,我们可以定义一个通用的函数模板(function template),而不是为每个类型都定义一个新函数。一个函数模板就是一个公式,可用来生成针对特定类型的函数版本:
template
int compare(const T& a, const T& b)
{
if(a < b)
return -1;
if(b < a)
return 1;
return 0;
}
泛型编程:编写与类型无关的逻辑代码,是代码复用的一种手段。模板是泛型编程的基础。
模板定义以关键字template开始,后跟一个模板参数列表列表,这是一个逗号分隔的一个或多个模板参数的列表,用<>包围起来。类型参数前必须使用关键字typename或者class。在模板参数列表中,这两个关键字的含义相同,可以互换使用。一个模板参数列表中可以同时使用这两个关键字:template
compare(const T&, const U&);
模板参数表示在类或函数定义中用到的类型或值。当使用模板时,我们(隐式地或显式地)指定模板实参,将其绑定到模板参数上。
模板函数也可以定义为inline函数,inline关键字必须放在模板形参表后面,返回值之前,不能放在template之前。(模板是一个蓝图,它本身不是类或者函数)
1、实例化函数模板
当我们调用一个函数模板时,编译器通常用函数实参来为我们推断模板实参。编译器用推断出的模板参数来为我们实例化一个特定版本的函数。当编译器实例化一个模板时,它使用实际的模板实参代替对应的模板参数来创建出模板的一个新实例。
以上编译器就实例出了两个不同版本的compare,对于第一个调用,编译器会编写并编译一个T被替换成int的compare的版本;对于第二个调用,编译器会生成另一个compare版本,其中T被替换成double。这些编译器生成的版本通常被称为模板的实例。
模板直到实例化时才会生成代码,这一特性影响了我们何时才会获知模板内代码的编译错误。通常,编译器会在三个阶段报告错误:
第一个阶段是在编译模板本身时。在这个阶段,编译器通常不会发现很多的错误。编译器可以检查语法错误,例如忘记分量或者变量名拼错等。
第二个阶段是编译器遇到模板使用时。对于函数模板调用,编译器通常会检查实参数目是否正确,它还能检查参数类型是否匹配。
第三个阶段是模板实例化时,只有这个阶段才能发现类型相关的错误,依赖于编译器如何管理实例化,这类错误可能在链接时才报告。
2、类型形参转换
一般不会转换实参以匹配已有的实例化,相反会产生新的实例。
编译器只会执行两种转换:
1)const转换:接收const引用或者const指针的函数可以分别用非const对象的引用或者指针来调用
2)数组或函数到指针的转换:如果模板形参不是引用类型,则对数组或函数类型的实参应用常规指针转换。数组实参将当做指向其第一个元素的指针,函数实参当做指向函数类型的指针。
3、实参推演
从函数实参确定模板形参类型和值的过程称为模板实参推演,多个类型形参的实参必须完全匹配。
4、模板形参说明
(1)模板形参表使用<>括起来
(2)和函数参数表一样,跟多个参数时必须用逗号隔开,类型可以相同也可以不相同
(3)模板形参表不能为空
(4)模板形参可以是类型形参,也可以是非类型新参,类型形参跟在class和typename后
(5)模板类型形参可作为类型说明符用在模板中的任何地方,与内置类型或自定义类型使用方法完全相同,可用于 指定函数形参类型、返回值、局部变量和强制类型转换
(6)模板形参表中,class和typename具有相同的含义,可以互换,使用typename更加直观。typename是作为 C++标准加入到C++中的,旧的编译器可能不支持。
5、模板函数特化有时候并不总是能够写出对所有可能被实例化的类型都最合适的模板,在某些情况下,同用模板定义对于某个类型可能是完全错误的,或者不能编译,或者做一些错误的事情。
可以对模板进行特化:
(1)关键字template后面接一堆空的尖括号<>
(2)再接模板名和一对尖括号,尖括号中指定这个特化定义的模板形参
(3)函数形参表
(4)函数体 template<>
返回值 函数名
{
// 函数体
}
特化的声明必须与特定的模板相匹配。
注意:
非类型模板参数:除了定义类型参数,还可以在模板中定义非类型参数。一个非类型参数表示一个值而非一个类型。我们通过一个特定的类型名而非关键字class或者typename来指定非类型参数。
例如:我们可以编写一个compare版本处理字符串字面值常量。这种字面值常量是const char的数组。由于不能拷贝一个数组,所以我们将自己的参数定义为数组的引用。我们希望能比较不同长度的字符串字面值常量,因此为模板定义了两个非类型的参数。第一个模板参数表示第一个数组的长度,第二个参数表示第二个数组的长度:
编译器就会实例化出这样的版本:
int compare(const char (&p1)[3], const char (&p2)[4]);
一个非类型参数可以是一个整型,或者是一个指向对象或函数类型的指针或引用。绑定到非类型整型参数的实参必须是一个常量表达式,绑定到指针或引用非类型参数的实参必须静态的生存期。我们不能用一个普通(非static)局部变量或动态对象作为指针或引用非类型模板参数的实参。指针参数也可以用nullptr或者一个值为0的常量表达式来实例化。
在模板定义内,模板非类型参数是一个常量值。在需要表达常量式的地方,可以使用非类型参数。例如,指定数组大小。