文章目录
第九章:模板
模板template是一种通用的描述机制,使用模板允许使用通用类型来定义函数或类。在使用时,通用类型可被具体的类型,如 int、double 甚至是用户自定义的类型来代替。模板引入一种全新的编程思维方式,称为“泛型编程”或“通用编程”。
1.为什么要定义模板
像C/C++/Java等语言,是编译型语言,先编译后运行。它们都有一个强大的类型系统,也被称为强类型语言,希望在程序执行之前,尽可能地发现错误,防止错误被延迟到运行时。所以会对语言本身的使用造成一些限制,称之为静态语言。与之对应的,还有动态语言,也就是解释型语言。如javascript/python/Go,在使用的过程中,一个变量可以表达多种类型,也称为弱类型语言。因为没有编译的过程,所以相对更难以调试。
例如,想要实现能够处理各种类型参数的加法函数
以前我们需要进行函数重载(函数名相同,函数参数不同)
int add(int x, int y)
{
return x + y;
}
double add(double x, double y)
{
return x + y;
}
long add(long x, long y)
{
return x + y;
}
string add(string x, string y)
{
return x + y;
}
模板:将数据类型作为参数
上面的问题用函数模板的方式就可以轻松解决:
//希望将类型参数化
//使用class关键字或typename关键字都可以
template <class T>
T add(T x, T y)
{
return x + y;
}
int main(void){
cout << add(1,2) << endl;
cout << add(1.2,3.4) << endl;
return 0;
}
函数模板的优点:
不需要程序员定义出大量的函数,在调用时实例化出对应的模板函数,更“智能”
2.模板的定义
模板作为实现代码重用机制的一种工具,它可以实现类型参数化,也就是把类型定义为参数,从而实现了真正的代码可重用性。(以前的代码可重用,例如函数重载,只是调用的时候代码可重用了,但是重载的代码还是很多的。)
模板可以分为两类,一个是函数模版,另外一个是类模板。通过参数实例化定义出具体的函数或类,称为模板函数或模板类。模板的形式如下:
模板参数是一个更大的概念,包含了类型参数和非类型参数,这里的T1/T2属于类型参数,代表了类型。
模板发生的时机是在编译时
模板本质上就是一个代码生成器,它的作用就是让编译器根据实际调用来生成代码。
编译器去处理时,实际上由函数模板生成了多个模板函数,或者由类模板生成了多个模板类。
函数模板:调用函数,在编译时会实例化出相应的模板函数。
(1)函数模板
由函数模板到模板函数的过程称之为实例化
过程:函数模板 → 生成相应的模板函数 → 编译 → 链接 → 可执行文件
//函数模板
template <class T>
T add(T t1, T t2){
return t1 + t2;
}
①隐式实例化
进行模板实例化时,并没有指明任何类型,函数模板在生成模板函数时通过传入的参数类型推导出模板类型,这种做法称为隐式实例化。
template <class T>
T add(T t1,T t2)
{ return t1 + t2; }
void test0(){
short s1 = 1, s2 = 2;
int i1 = 3, i2 = 4;
long l1 = 5, l2 = 6;
double d1 = 1.1, d2 = 2.2;
cout << "add(s1,s2): " << add(s1,s2) << endl;
cout << "add(i1,i2): " << add(i1,i2) << endl;
cout << "add(l1,l2): " << add(l1,l2) << endl;
cout << "add(d1,d2): " << add(d1,d2) << endl;
}
②显式实例化
在使用函数模板时还可以在函数名之后直接写上模板的类型参数列表,指定类型,这种用法称为显式实例化。
template <class T>
T add(T t1,T t2)
{ return t1 + t2; }
void test0(){
int d1 = 1.5, i2 = 2.5;
cout << "add(d1,d2): " << add<int>(d1,d2) << endl; //显式实例化
}
①函数模板的重载
函数模板的重载分为:
(1)函数模板与函数模板重载
(2)函数模板与普通函数重载
<1>函数模板与函数模板重载
代码链接:https://github.com/WangEdward1027/Object-Oriented/blob/main/template/TemplateOverload.cpp
隐式实例化有冲突时,需要显式实例化。
但显式实例化可能选错类型,造成错误,精度损失。
于是考虑,重载函数模板:有两个模板参数。
缺省,第一个参数如果匹配,缺省的第二个参数会自动推导。
编译器优先选择:
①参数进行类型转换少的模板。
②如果类型转换都一样多,比如第一个参数和模板要求不一样,则选择模板参数少的模板。
在一个源文件中定义多个通用模板的写法应该谨慎使用(尽量避免),如果实在需要也尽量使用隐式实例化的方式进行调用,编译器会选择参数类型最匹配的模板(通常是参数类型需要更少转换的模板)。
函数模板与函数模板重载的条件:
(1)首先,名称必须相同(显然)
(2)模板参数列表中的模板参数在函数中所处位置不同 —— 但不建议进行这样的重载。
(3)模板参数的个数不一样时,可以构成重载(相对常见)
(2)的例子
template <class T1, class T2>
T1 add(T1 t1, T2 t2)
{
cout << "模板一" << endl;
return t1 + t2;
}
template <class T1, class T2>
T1 add(T2 t2, T1 t1)
{
cout << "模板二" << endl;
return t1 + t2;
}
int a = 10;
double b = 1.2;
cout << add(a,b) << endl; //error 冲突,两个模板都能行得通,且同样优先级
cout << add<int>(a,b) << endl; //模板一
cout << add<double>(a,b) << endl; //模板二。缺省,T2推导出int
<2>函数模板与普通函数重载
代码链接:https://github.com/WangEdward1027/Object-Oriented/blob/main/template/TemplateOverload2.cpp
普通函数优先于函数模板执行——因为普通函数更快
如果没有普通函数,就会调用上面的函数模板,实例化出相应的模板函数。尽管s1/s2的类型相同,也是可以使用该模板的。
—— T1/T2并不一定非得是不同类型,能推导出即可。
当然,如果采用显示实例化的方式调用,肯定是调用函数模板。
②头文件与实现文件形式 (重要)
从原理上进行分析,函数模板定义好之后并不会直接产生一个具体的模板函数,只有在调用时才会实例化出具体的模板函数。
解决方法 —— 在”实现文件“中要进行调用,因为有了调用才有推导,才能由函数模板生成需要的函数
//add.cc
template <class T>
T add(T t1, T t2)
{
return t1 + t2;
}
//在这个文件中如果只是写出了函数模板的实现
//并没有调用的话,就不会实例化出模板函数
void test1(){
cout << add(1,2) << endl;
}
但是在“实现文件”中对函数模板进行了调用,这种做法不优雅 。
设想:如果在测试文件调用时,在推导的过程中,看到的是完整的模板的代码,那么应该可以解决问题
//add.h
template <class T>
T add(T t1, T t2);
#include "add.cc"
可以在头文件中加上#include “add.cc”,即使实现文件中没有调用函数模板,单独编译testAdd.cc,也可以发现问题已经解决。
因为本质上相当于把函数模板的定义写到了头文件中。
总结:
(1)对模板的使用,必须要拿到模板的全部实现,如果只有一部分,那么推导也只能推导出一部分,无法满足需求。
(2)换句话说,就是模板的使用过程中,其实没有了头文件和实现文件的区别,在头文件中也需要获取模板的完整代码,不能只有一部分。
C++的标准库都是由模板开发的,所以经过标准委员会的商讨,将这些头文件取消了后缀名,与C的头文件形成了区分;这些实现文件的后缀名设为了tcc
C++标准委员会规定:使用模板时,头文件不加后缀名,实现文件后缀是.tcc。头文件中包含了实现文件。
③特化模板
代码链接:https://github.com/WangEdward1027/Object-Oriented/blob/main/template/TemplateSpecialization.cpp
内存泄露。记得要delete。
特化模板:template <>
//特化模板
//这里就是告诉编译器这里是一个模板
template <>
const char * add<const char *>(const char * p1,const char * p2){
//先开空间
char * ptmp = new char[strlen(p1) + strlen(p2) + 1]();
strcpy(ptmp,p1);
strcat(ptmp,p2);
return ptmp;
}
void test0(){
//通用模板无法应对如下的调用
const char * p = add<const char *>("hello",",world");
cout << p << endl;
delete [] p;
}
注意:
使用特化模板,前提是有通用模板。
特化模板的形式(函数名、参数列表),要与通用模板一致。
特化模板,就是为了解决通用模板无法处理的特殊类型的操作。
<1>模板的全特化 (完全特化)
<2>模板的偏特化,部分特化 (不完全特化的模板)
④使用模板的规则 (重要)
1.一个文件中避免定义多个通用模板 (不确定应该调用哪个模板)
2.调用函数模板,尽量隐式调用,让编译器推导出类型
3.无法使用隐式调用的场景只指定必须要指定的类型
4.需要使用特化模板的场景就根据特化模板将类型指定清楚。
3举例:
特例化< >中,只填返回值类型,而不是填三个参数,否则指定太多了,无法使用隐式实例化。
只在必须显式实例化的地方写,只指定必要的。
4.举例
要特化的时候,在定义和调用的时候,加上<>,告诉编译器这是特化模板。
⑤模板的参数类型
1.模板的参数类型:
(1)类型参数 (模板参数)
之前的T/T1/T2等等称为模板参数,也称为类型参数,类型参数T可以写成任何类型
(2)非类型参数 (non-type template parameter)
非类型参数,必须是整型数据, char/short/int/long/size_t等。不能是float、double等。
2.模板参数列表、函数参数列表
<>叫模板参数列表。除了可以放类型参数,还可以放非类型参数
()叫函数参数列表
非类型参数:
①传入时指定
②加默认值
优先级:指定的类型 > 推导出的类型 > 类型的默认参数
轮到默认值的情况:前两种都走不通
不过尽量从右往左赋默认值:
⑥成员函数模板
显式实例化
默认值
以往成员函数能用的 static、const、&,在模板中都可以用
只有虚函数不可以用模板。模板是编译时生效,虚函数是运行时生效。
声明与实现分离:
(2)类模板
(1)类模板定义
代码链接:https://github.com/WangEdward1027/Object-Oriented/blob/main/template/classTemplate.cpp
https://github.com/WangEdward1027/Object-Oriented/blob/main/template/classTemplate2.cpp
类模板的定义:
template <class/typename T, ...>
class 类名{
//类定义......
};
示例,用类模板的方式实现一个Stack类,可以存放任意类型的数据
——使用函数模板实例化模板函数使用类模板实例化模板类
类模板的成员函数如果放在类模板定义之外进行实现,需要注意:
(1)需要带上template模板形参列表(如果有默认参数,此处不要写,写在声明时就够了)
(2)在添加作用域限定时需要写上完整的类名和模板实参列表
加上<>,表明这是一个类模板,而不是一个普通类。
//TemplateClass.cc
(3)可变参数模板
可变参数模板(variadic templates)是 C++11 新增的最强大的特性之一,它对参数进行了高度泛化,它能表示0到任意个数、任意类型的参数。由于可变参数模板比较抽象,使用起来需要一定的技巧,所以它也是 C++11 中最难理解和掌握的特性之一。
可变参数模板和普通模板的语义是一样的,只是写法上稍有区别,声明可变参数模板时需要在typename 或 class 后面带上省略号 “…”
//可变参数模板
template <class ...Args>
void func(Args ...args);
//普通函数模板做对比
template <class T1,class T2>
void func(T1 t1, T2 t2);
Args叫做模板参数包,相当于将 T1/T2/T3/…等类型参数打了包
args叫做函数参数包,相当于将 t1/t2/t3/…等函数参数打了包
省略号写在参数包的左边,代表打包
例如,我们在定义一个函数时,可能有很多个不同类型的参数,不适合一一写出,就可以使用可变参数模板的方法。
利用可变参数模板输出参数包中参数的个数:
#include <iostream>
using std::cout;
using std::endl;
template <class ...Args>//Args 模板参数包
void display(Args ...args)//args 函数参数包
{
//输出模板参数包中类型参数个数
cout << "sizeof...(Args) = " << sizeof...(Args) << endl;
//输出函数参数包中参数的个数
cout << "sizeof...(args) = " << sizeof...(args) << endl;
}
void test0(){
display();
display(1,"hello",3.3,true);
display(1,"hello",3.3,true,'a',2);
}
int main(void){
test0();
return 0;
}
——试验:希望打印出传入的参数的内容
就需要对参数包进行解包。每次解出第一个参数,然后递归调用函数模板,直到递归出口
//递归的出口
void print(){
cout << endl;
}
//重新定义一个可变参数模板,至少得有一个参数
template <class T,class ...Args>
void print(T x, Args ...args)
{
cout << x << " ";
print(args...); //省略号在参数包右边
}
省略号写在参数包的左边,代表打包
省略号写在参数包的右边,代表解包