1.泛型编程
泛型编程:编写与类型不管的同样代码,是代码复用的一种手段,模板是泛型编程的基础。
不是使用泛型编程,使用函数重载可以实现实现一个通用的交换函数:
1 | void Swap(int& left, int& right) { |
由此可见:
- 重载的函数仅仅只是类型不同,代码的复用率表弟,只要有新的类型出现,就需要增加对用的函数
- 代码的可维护性比较低,一个出错可能所有的重载出错
由此可见利用泛型编程,让编译器根据不通的类型利用该模板来生成相应的代码是多么高效的。
2.函数模板
函数模板代表了一个函数家族,该函数模板与类型无关,在使用时被参数化,根据实参类型产生函数的特定
类型版本。
2.1 函数模板格式
将上面的变换函数写成一个函数模板:
1 | template<typename T> |
template<typename T1, typename T2,……,typename Tn>
返回值类型 函数名(参数列表){}
typename 是用来定义模板参数关键字,也可以使用class,不能使用struct代替class
2.2 函数的原理
在编译器编译阶段,对模板函数的使用,编译器需要根据传入的实参类型来推演生成相对应参数的函数以供调用,如图,当用double类型使用函数模板时,编译器通过对实参的推演,将T确定为double类型,然后产生一份专门处理double类型的代码。
2.3 函数模板的实例化
不同类型的参数使用模板时,称为函数模板的实例化。模板参数实例化分为:隐式实例化和显式实例化。
1. 隐式实例化:让编译器根据实参推演模板的实际类型
1 | template<class T> T Add(const T& left, const T& right) { |
2.显式实例化:在函数名后的<>中指定模板参数的实际类型
1 | int main(void) { |
如果类型不匹配,编译器会尝试进行隐拾类型转换,如果无法转换成功编译器将会保错。
2.4 模板参数的匹配原则
- 一个非模板函数可以和一个同名的函数模板同时存在,而且该函数模板还可以被实例化为非模板函数。
- 对于非模板函数和同名函数模板,如果其他条件都相同,在调用时会优先调用非模板函数而不是该模板函数残生的一个实例。
- 模板函数不允许自动类型转换,但是普通函数可以进行自动类型转换。
3.类模板
3.1 类模板的定义格式
1 | template<class T1, class T2, ..., class Tn> |
在这里实现一个动态顺序表的模板:
1 | // 动态顺序表 |
类模板中的函数在类外面定义时,需要加模板参数列表
以上的vector不是具体的类,时编译器根据被实例化的类型生成具体类的摸具。
3.2类模板的实例化
类模板实例化与函数模板实例化不同,类模板实例化需要在类模板名字后跟<>,然后将实例化的类型放在<>中即可,类模板名字不是真正的类,而实例化的结果才是真正的类。
1 | int main(){ |
4. 非类模板参数
模板参数分为类型参数和非类型参数。
类型性参数:出现在模板参数列表中,跟class或者typenname之类的参数类型名称。
非类型形参:用一个常量作为(函数)模板的一个参数,在类(函数)的模板中可将参数当做常量使用。(类似于定义了一个宏)
1 | template<class T, size_t N = 10> |
这个功能比较鸡肋,因为申请的空间是在栈上开辟的。
- 浮点型、类对象以及字符串是不允许组为非类型模板参数的。
- 非类型的模板参数必须在编译期就能确认结果。
5.模板的特化
5.1特化
使用模板可以是实现一些与类型无关的代码,但是对于一些特殊类型的可能会得到一些错误的结构,比如:
1 | template<class T> |
调用Test1返回的结果:
调用Test2返回的结果:
可以观察到用这个模板函数对两个字符串进行比较,得到的返回值和字符的内容没有任何关系,之和指针的地址有关系,这样就不能反映出这个代码的真正的功能,这时候就要对模板进行特化。
5.1 函数模板的特化
特化步骤:
1. 必须要现有一个基础的函数模板
2. 关键字templata后接一对空的尖括号<>
3. 函数名后跟一对尖括号,尖括号指定需要特化的类型
4. 函数形参表: 必须要和模板函数的基础参数类型完全相同,如果不同编译器可能会报一些奇怪的错误。
将上面的比较大小的模板进行特化:
1 | template<> |
5.2 类模板的特化
- 全特化:全特化就是将模板参数泪飙中的所有参数都确认化
- 偏特化:
1. 部分特化,将参数模板中的一部分参数特化。 2. 参数更进一步的限制:偏特化并不仅仅是指特化部分参数,而是针对模板参数更进一步的条件限制所设计出来的一个特化版本
6. 类模板特化应用之内存萃取
STL库里对拷贝分为两种情况处理:
内置类型:memcpy 一片内存完整的拷贝,效率高,时间复杂度O(1);
自定义类型:赋值拷贝,一个一个赋值,要走一边循环,效率低,时间复杂度O(N);
那么每次在拷贝的时候如何判断区分是走memcpy还是辅助拷贝,有两种方法:
增加bool类型(IsPODType)区分自定义和内置类型
POD: Planin Old Data 代表内置类型
使用函数区分内置类型和自定义类型
RTTI:运行时类型识别
1 | bool IsPODType(const char* strType) { |
通过typeid来确认所拷贝对象的实际类型,然后再在内置类型集合中枚举是其否出现过,即可确认所拷贝元素的类型为内置类型或者自定义类型,但是这种方法效率比较低,时间复杂度为O(N^2)。
内存萃取
先定义两个结构体判断所所传的类型是内置类型还是自定义类型
1 | //内置类型 |
然后定义一个模板
1 | template <class T> |
然后进行对所有内置类型模板特化:
1 | //char类型 |
T为int:TypeTraits
_IsPODType刚好为类TrueType,而TrueType中Get函数返回true,内置类型使用memcpy方式拷贝
T为string:TypeTraits
刚好为类FalseType,而FalseType中Get函数返回true,自定义类型使用赋值方式拷贝
总结为以下几点:
- 使用类模板和模板特化技术定义类型萃取类typetraits,内部类型定义类型falsetype
- 对于内置类型,通过模板特化自定义类型typrtraits,内部定义类型truetype
- 编译期间通过输入的类型生成对应的的typetraits,调用truetype或者falsetype的get()方法确认具体类型是否为自定义类型,根据返回值结果决定拷贝方式
内存萃取的优点:效率高,编译时确定类型,不占用运行时间