泛型程序设计(generic programming)是一种算法在实现时不指定具体要操作的数据的类型的程序设计方法。所谓 "泛型",指的是算法只要实现一遍,就能适用于多种数据类型。泛型程序设计方法的优势在于能够减少重复代码的编写。
泛型程序设计的概念最早出现于 1983 年的 Ada 语言,其最成功的应用就是 C++ 的标准模板库(STL)。也可以说,泛型程序设计就是大量编写模板、使用模板的程序设计。泛型程序设计在 C++ 中的重要性和带来的好处不亚于面向对象的特性。
在 C++ 中,模板分为函数模板和类模板两种。熟练的 C++ 程序员,在编写函数时都会考虑能否将其写成函数模板,编写类时都会考虑能否将其写成类模板,以便实现重用。
C++ 函数模板
考虑一下交换两个变量的值的函数,如下,我们可以通过定义不同类型的重载函数实现对不同类型的两个值的交换。下面我们定义了四个名字相同、参数列表不同的函数。
1 | // 交换 int 变量的值 |
这些函数虽然在调用时方便了一些,但从本质上说还是定义了四个功能相同、函数体相同的函数,只是数据的类型不同而已,这看起来有点啰嗦,能不能把它们压缩成一个函数呢?
能,可以借助函数模板。
我们知道,数据的值可以通过函数参数传递,在函数定义时数据的值是未知的,只有等到函数调用时接收了实参才能确定其值。这就是值的参数化。
在 C++ 中,数据的类型也可以通过参数来传递,在函数定义时可以不指明具体的数据类型,当发生函数调用时,编译器可以根据传入的实参自动推断数据类型。这就是类型的参数化。
值(Value)和类型(Type)是数据的两个主要特征,它们在 C++ 中都可以被参数化。
所谓函数模板,实际上是建立一个通用函数,它所用到的数据的类型(包括返回值类型、形参类型、局部变量类型)可以不具体指定,而是用一个虚拟的类型来代替(实际上是用一个标识符来占位),等发生函数调用时再根据传入的实参来逆推出真正的类型。这个通用函数就称为函数模板(Function Template)。
在函数模板中,数据的值和类型都被参数化了,发生函数调用时编译器会根据传入的实参来推演形参的值和类型。换个角度说,函数模板除了支持值的参数化,还支持类型的参数化。
一旦定义了函数模板,就可以将类型参数用于定义函数定义和函数声明了。说得直白一点,原来使用 int、float、char 等内置类型的地方,都可以用类型参数来代替。
下面来实践一下,将上面的四个 Swap() 函数压缩为一个函数模板。
1 |
|
运行结果:
1 | 200, 100 |
请读者重点关注第 4 行代码。template
是定义函数模板的关键字,它后面紧跟尖括号
<>
,尖括号包围的是类型参数(也可以说是虚拟的类型,或者说是类型占位符)。typename
是另外一个关键字,用来声明具体的类型参数,这里的类型参数就是
T
。从整体上看,template<typename T>
被称为模板头。
模板头中包含的类型参数可以用在函数定义的各个位置,包括返回值、形参列表和函数体;本例我们在形参列表和函数体中使用了类型参数 T。
类型参数的命名规则跟其他标识符的命名规则意义,不过使用 T、T1、T2 Type 等已经成为了一种惯例。
定义了函数模板之后,就可以像调用普通函数一样来调用它们了。
引用形参版本:
1 |
|
引用不但使得函数定义简洁明了,也使得调用函数方便了很多。整体来看,引用让编码更加漂亮。
下面我们来总结一下定义模板函数的语法:
1 | template<typename 类型参数1, typename 类型参数2, ...> 返回值类型 函数名(形参列表) { |
类型参数可以有多个,它们之间以逗号 ,
分隔。类型参数列表以 <>
包围,形式参数列表以
()
包围。
typename
关键字也可以使用 class
关键字替代,它们没有任何区别。C++
早期对模板的支持并不严谨,没有引入新的关键字,而是用 class
来指明类型参数,但是 class
关键字本来已经用在类的定义中了,这样做显得不太友好,所以后来 C++
又引入了一个新的关键字
typename
,专门用来定义类型参数。不过至今仍然有很多代码在使用
class
关键字,包括 C++ 标准库、一些开源程序等。
上面的 Swap
也可以使用 class
来指明类型参数:
1 | template<class T> void Swap(T &a, T &b) { |
除了将 typename
替换为
class
,其他都是一样的。
为了加深对函数模板的理解,我们再来看一个求三个数的最大值的例子:
1 |
|
输出:
1 | 12 34 100 |
函数模板也可以提前声明,不过声明时需要带上模板头,并且模板头和函数定义(声明)是一个不可分割的整体,它们可以换行,但中间不能有分号。