16.3 模板语法

template这个关键字会告诉编译器,随后的类定义将操作一个或更多未指明的类型。当由这个模板产生实际类代码时,必须指定这些类型以使编译器能够替换它们。

下面是一个说明模板语法的小例子,它产生一个带有越界检查的数组。

16.3 模板语法 - 图1

16.3 模板语法 - 图2

它看上去像一个普通的类,除了下面一行以外:

16.3 模板语法 - 图3

这里T是替换参数,它代表一个类型名称。在容器类中,它将出现在那些原本由某一特定类型出现的地方。

在Array中,其元素的插入和取出都用相同的函数—即重载的operator[]来实现。它返回一个引用,因此可被用于等号的两边(即,可以是左值也可以是右值)。注意,当下标值越界时,用require()函数输出提示信息。因为operator[]是内联的,所以用这种方法来保证不发生数组下标越界现象,随后在提交代码时去掉require()。

在main()中,我们看到可以非常容易地创建包含不同类型的Array。代码如下:

16.3 模板语法 - 图4

这时,编译器两次扩展了Array模板[这被称为实例化(instantiation)],创建两个新的生成类(generated class),可以把它们看做Array_int和Array_float(不同的编译器对名称有不同的修饰方法)。这些类就像手工创建的一样,只是这里是当定义了对象ia和fa后由编译器来创建这些类。我们还会注意到,编译器避免了或者连接器合并了类的重复定义。

16.3.1 非内联函数定义

当然,有时我们希望有非内联成员函数的定义。这时编译器需要在成员函数定义之前看到template声明。下面在前述例子的基础上加以修正来说明非内联函数的定义。

16.3 模板语法 - 图5

16.3 模板语法 - 图6

注意在引用模板的类名的地方,必须伴有该模板的参数列表,例如在Array<T>:operator[]中。可以想象,在内部,使用模板参数列表中的参数修饰类名,以便为每一个模板实例产生惟一的类名标识符。

16.3.1.1 头文件

即使是在创建非内联函数定义时,我们还是通常想把模板的所有声明和定义都放入一个头文件中。这似乎违背了通常的头文件规则:“不要放置分配存储空间的任何东西”(这条规则是为了防止在连接期间的多重定义错误),但模板定义很特殊。在template<……>之后的任何东西都意味着编译器在当时不为它分配存储空间,而是一直处于等待状态直到被一个模板示例告知。在编译器和连接器中有机制能去掉同一模板的多重定义。所以为了使用方便,几乎总是在头文件中放置全部的模板声明和定义。

有时,也可能为了满足特殊的需要(例如,强制模板示例仅存在于单个的Windows dll文件中)而要在一个独立的cpp文件中放置模板的定义。大多数编译器有一些机制允许这么做;我们将必须检查我们的特定编译器的说明文档以便使用它。

有些人认为,在实现中将所有源代码放在头文件中,如果有人从我们这里买到库,则他们就有条件盗窃和修改代码。这可能是一个问题,但它依赖于我们看待这个问题的方法:他们买的是产品还是服务?如果是产品,我们就必须为保护它做一些事情,或许我们不想给出源代码,而只给出编译过的代码。但是许多人把软件看做服务,甚至是预约服务。消费者想要我们的专门技术,想要我们继续维护这段可重用的代码,所以他们没有必要这样做,因此他们可以集中精力做他们的事情。我个人认为,大多数消费者将我们看做有价值的资源,不希望危害他们与我们之间的关系。至于少数想盗窃而不是购买或做独创工作的人,他们大概无论如何也不能与我们相处。