16.2 模板综述

现在出现了一个问题。IntStack可存放整数,但是也可能希望有一个栈可存放造型、航班、植物等数据对象。用强调重用性的语言每次从头重新开发代码,不是一个明智的办法。应该有更好的方法。

有3种源代码重用的方法:C方法,这里列出是为了对照;对C++产生过重大影响的Smalltalk方法;C++的模板方法。

(1)C方法

毫无疑问,应该摒弃C方法,这是由于它表现繁琐、易发生错误、缺乏美感。用这种方法,需要拷贝Stack的源码并对其进行手工修改,这样就会引进新的错误。这是非常低效的技术。

(2)Smalltalk方法

Smalltalk(以及之后的Java)方法是通过继承来实现代码重用的,既简单又直观。为此,每个容器类包含通用的基类Object的项目(类似于第15章最后的例子)。Smalltalk的基类库十分重要,完全不需要从头创建类。相反,创建一个新类必须从已有类中继承,不能随意创建。可以从类库中选择功能和需求尽可能接近的一个已有类作为父类,并在对父类的继承中加以修正从而创建一个新类。很明显,这种方法由于可以减少我们的工作量,因而提高了我们的效率(这也说明了为什么需要花大量的时间去学习Smalltalk类库才能成为熟练的Smalltalk程序员)。

但是,这也意味着Smalltalk的所有类都是单个继承树的一部分。当创建新类时必须继承树的某一枝。树的大部分已经存在(它是Smalltalk的类库),树的根称为Object—这是每个Smalltalk容器所包含的同一个类。

这是一种单纯的技巧,因为Smalltalk(和Java[1])类层次上的任何类都源于Object的派生,所以任何容器可容纳任何类(包括容器本身)。这种基于通用的基类(常称为Object,在Java中也有类似情况)的单树形层次类型称为“基于对象的层次结构”。我们可能听说过这个概念,并猜想这是另一个OOP的基本概念,就像“多态性”一样。但实际上,这仅仅意味着以Object(或相近的名称)为根的树形类结构和包含Object的容器类。

因为Smalltalk类库的发展史比C++更长久,且早期的C++编译器没有容器类库,所以C++能将Smalltalk类库的良好思想加以借鉴。这种借鉴出现在早期的C++实现中[2],由于它表现为一个有效的代码实体,因此许多人开始使用它,但在使用容器类的过程中发现了一个问题。

该问题在于,在Smalltalk(和我所知道的许多其他OOP语言)中,所有的类都自动地从单个层次结构中派生而来,但在C++中则不行。我们可能本来已经拥有了完善的基于对象的层次结构以及它的容器类,而且还可能从其他不用这种层次结构的供应商那里购买到一组类,如形体类、航班类等(为了使用层次结构而增加了开销,这是C程序员不愿意做的事情)。我们如何把一个单独的类树插入到我们的基于对象的层次结构中的容器类之中呢?这个问题如下所示:

16.2 模板综述 - 图1

因为C++支持多个独立层次结构,所以Smalltalk的“基于对象的层次结构”在此不适用。

解决方案似乎是明显的。如果我们有许多继承层次结构,就应当能从多个类继承:多重继承可以解决上述问题。所以我们应该按下述的方法去实施(一个类似的例子已在第15章结尾处给出):

16.2 模板综述 - 图2

现在,OShape具有了Shape的特点和行为,但它也是从Object派生而来的,所以可将其置于Container内。额外的继承也必然进入了OCircle和OSquare等,这样这些类才能向上类型转换为OShape,并因而保持正确的行为。我们可以看到,事情正在迅速变得混乱。

编译器供应商发明了他们自己的基于对象的容器类层次结构,并将它们加入到他们的编译系统中,这些层次结构中的大多数可以用模板版本替代。我们可以对多重继承是否可以解决大多数编程问题进行争论,但是在本书的第2卷中将会看到,除某些特殊情况外,它的复杂性是可以很好避免的。

16.2.1 模板方法

在Stroustrup的最初著作[3]中阐述了替换基于对象层次的一种更可取的选择。创造容器类作为参数化类型的大型预处理宏,这些参数能替代为所希望的类型。当我们打算创建一个容器存放某个特别类型时,应当使用一对宏调用。

不幸的是,这种方法在当时被所有已有的Smalltalk文献和程序设计经验弄混淆了,加之它确实有点难处理,所以当时基本上没有什么人用它。

在此期间,Stroustrup和贝尔实验室的C++小组对原先的宏方法进行了修正,对其进行了简化并将它从预处理器域移入到编译器中。这种新的代码替换装置被称为模板[4],而且它表现了完全不同的代码重用方法:模板对源代码进行重用,而不是通过继承和组合重用目标代码。容器不再存放称为Object的通用基类,而是存放一个未指明的参数。当用户使用模板时,参数由编译器(by the compiler)来替换,这非常像原来的宏方法,但却更清晰、更容易使用。

现在,可以不必在使用容器类时担忧继承和组合了,可以采用容器的模板并且为具体问题复制出特定的版本,就像下图所示:

16.2 模板综述 - 图3

编译器会为我们做这些工作,而我们最终是以所需要的容器去做我们的工作,而不是用那些令人头疼的继承层次。在C++中,模板实现了参数化类型(parameterized type)的概念。模板方法的另一个优点是,使对继承不熟悉、不适应的新程序员也能正确地使用密封的容器类(就像在本书中对vector所做的那样)。

[1]在Java中,基本数据类型是一个例外,出于效率的考虑,这里有一些非Object类型。

[2]OOPS库,由Keith Gorlen在NIH时创建。

[3]《C++程序设计语言》(The C++Pr ogramming Language),由Bjarne Stroustrup著(第1版,Addison-Wesley公司1986年出版)。

[4]模板的思想类似于ADA的泛型(generic)。