模板
泛型编程
泛型编程(generic programming)
- 独立于任何特定数据类型的编程,这使得不同类型的数据(对象)可以被相同的代码操作
在 C++ 中,使用模板(template)来进行泛型编程,包括
- 函数模板(Function template)
- 类模板(Class template)
当从通用代码创建实例代码时,具体数据类型才被确定
泛型编程是一种编译时多态性(静态多态)。其中,数据类型本身是参数化的,因而程序具有多态性特征
实例化
实例化(Instantiation):由编译器将通用模板代码转换为不同的实例代码的过程称为实例化
函数模板(Function template)
概念
用相同的处理过程,处理不同类型的数据
- 减少代码
- 甚至能处理编程时未知的数据类型
已知函数,类,未知处理的数据类型,模板
已知基类的虚函数,未知派生类的具体实现,覆盖(多态)
已知函数功能,未知具体参数类型与组合,重载
举例
1 | void swap(int& v1, int& v2) { |
1 | void swap(double& v1, double& v2) { |
1 | void swap(string& v1, string& v2) { |
此三个函数除了所处理对象的类型不同之外,代码几乎完全相同。即,三个函数功能类似。以下函数模板可以涵盖以上三函数的作用:
1 | template<typename T> |
注意:程序有些 bug。当 T 存在默认构造函数时,编译错误。
正确的写法: T tmp = v1; 即使用 copy 构造,来避免使用默认初始化
一般形式
1 | template<模板形参表> 返回值类型 函数名 (形参列表) { |
- 模板形参表不能为空
- 形参列表必须包含模板形参表中出现的所有形参
模板形参表的形式:
- typename 模板形参1, typename 模板形参2, …
实例化(instantiation)
函数模板的使用形式与普通函数调用相同。
1 | int main() { |
调用函数模板的过程:
- 模板实参推断(template argument deduction):编译器根据函数调用中所给出的实参的类型,确定相应的模板实参
- 函数模板的实例化(instantiation):模板实参确定之后,编译器就使用模板实参代替相应的模板形参,产生并编译函数模板的一个特定版本(称为函数模板的一个实例(instance))(注意:此过程中不进行常规隐式类型转换)
显示(全)模板特化
- 在模板当中有些特殊的类型,当想要针对特殊的类型进行一些特殊的操作,这时候就可以用模板的特化
- 在正常的模板下面接着编写代码,写一个空的template<>然后写个具体的函数代码来补充
- 如实例所示,当传入的实参类型是int类型,就执行模板的特化部分,而非int类型执行正常的模板推断
1 | template <typename T> void swap(T& v1, T& v2) { |
模板重载
对函数模板进行重载:
- 定义名字相同而函数形参表不同的函数模板
- 或者定义与函数模板同名的非模板函数(正常函数),在其函数体中完成不同的行为
编译器是如何确定调用的是这么多同名函数中的哪一个呢?
如何确定调用哪个函数
函数调用的静态绑定规则(重载协议):
- 如果某一同名非模板函数(指正常的函数)的形参类型正好与函数调用的实参类型匹配(完全一致),则调用该函数。否则,进入第2步
- 如果能从同名的函数模板实例化一个函数实例,而该函数实例的形参类型正好与函数调用的实参类型匹配(完全一致),则调用该函数模板的实例函数。否则,进入第3步
- 在步骤2中:首先匹配函数模板的特化,在匹配非指定特殊的函数模板
- 对函数调用的实参进行隐式类型转换后与非模板函数再次进行匹配,若能找到匹配的函数则调用该函数。否则,进入第4步
- 提示编译错误
1 | // 函数模板demoPrint |
1 | /* 结果 */ |
结论:函数模板是不进行隐式转换的,只有非函数模板才进行隐式转换
类模板
使用情景:定义可以存放任意类型对象的通用容器类
- 定义一个栈(stack)类,既可用于存放int型对象,又可用于存放float、double、string…甚至任意未知类型的元素
- 定义一个队列(queue)类,即可用于存放int型对象,又可用于存放float、double、string…甚至任意未知类型的元素
实现方式:为类声明一种模板,使得类中的某些数据成员、某些成员函数的参数、某些成员函数的返回值,能取任意类型(包括基本类型和用户自定义类型)
定义方式
类模板的一般形式:
1 | template <模板形参表> |
在类模板外定义成员函数的一般形式:
1 | template <模板形参表> |
其中模板形参表的形式为:template <typename 类型参数1, typename 类型参数2, …>
(注:模板形参每项是非类型形参、类型形参、模板形参之一。)
示例:链表实现的栈类模板
类模板的实例化
- 类模板是一个通用类模型,而不是具体类,不能用于创建对象,只有经过实例化后才得到具体类,才能用于创建对象
- 实例化的一般形式:
- 类模板名 < 模板实参表 >
- 模板实参是一个实际类型,如int,double等
- 一个类模板可以实例化为多个不同的具体类
- Stack
stack_int - Stack
stack_double - Stack
stack_string
- Stack
类模板的文件组织形式
一般而言,调用函数时,编译器只需要看到函数的声明即可。所以可以把函数的声明放在 .h 文件中,实现在 .cpp 的实现文件中,使用函数的地方#include 函数的 .h 文件即可
对于模板则不同,要进行实例化,编译器必须能够访问模板定义的源代码
为了在模板中也实现一般的声明定义分离,C++提供了两种模板的编译模型:
包含编译模式(inclusion compilation model)
- 要求:在函数模板或类模板成员函数的调用点,相应函数的定义对编译器必须是可见的
- 实现方式:在头文件中用#include包含实现文件(也可将模板的实现代码直接放在头文件中)
1
2
3
4
5
6
7
8
9//genericStack.h
类模板的定义和实现代码1
2
3
4
5
6
7
8//client.cpp客户代码
int main()
{
Stack<int> stack;
for (int i = 1; i < 9; i++) stack.push(i);
}
分离编译模式(separate compilation model)
- 要求:声明和定义分离,程序员在实现文件中使用保留字export告诉编译器,需要记住哪些模板定义。
- 不是所有编译器都支持该模式
非类型模板形参
- 两类模板形参:类型模板形参和非类型模板形参
非类型模板形参:- 相当于模板内部的常量
- 形式上类似于普通的函数形参
- 对模板进行实例化时,非类型形参由相应模板实参的值代替
- 对应的模板实参必须是编译时常量表达式
示例1:以数组实现的栈类模板
示例2:打印函数
不使用非模板形参实现
1 | template <typename T> |