5.5 模板编程中的习语

语言是表达思想的一种工具,新的编程语言特征总是会产生新的程序设计技术。本节讨论一些经常使用的模板编程用语,自模板被引入C++语言,这些用语就已经出现并应用了好多年了。[1]

5.5.1 特征

特征模板技术,最先由Nathan Myers倡导,它是一种将与某种类型相关联的所有声明绑定在一起的实现方式。本质上说,使用特征技术,可以以一种灵活的方法从它们的语境中将这些类型和值进行“混合与匹配”,同时又使得程序的代码灵活易读并且易于维护。

一个最简单的特征模板的例子是定义在<limits>中的numeric_limits类模板。这个基本模板的定义如下:

5.5 模板编程中的习语 - 图1

5.5 模板编程中的习语 - 图2

<limits>头文件为所有基本数字类型定义了特化(当is_specialized成员被设为true时)。例如,若想得到浮点数字系统的double版本的基类型,可以使用表达式numeric

limits<double>:radix。为了得到有用的最小整数值,可以使用numeric_limits<int>:min()。在程序中,并非向所有的numeric_limits成员都提供了基本类型。(例如,epsilon()只对浮点数类型有意义。)

有些值总是整数,它们是numeric_limits的静态数据成员。有些可能不总是整数值,例如float的最小值,它们作为静态内联成员函数实现。这是因为C++只允许在类定义中初始化整数(integral)静态数据成员常量。

在第3章中读者看到了字符串类如何使用特征技术控制字符处理函数。std:string类和std:wstring类是std:basic_string模板的特化,它的定义如下所示:

5.5 模板编程中的习语 - 图3

模板参数charT代表了基础字符类型,它通常是char类型或wchar_t类型。基本的char_traits模板是典型的空模板,标准库提供了对char和wchar_t进行的特化。下面是根据C++标准提供的一个char_traits<char>特化:

5.5 模板编程中的习语 - 图4

5.5 模板编程中的习语 - 图5

basic_string类模板使用这些函数,用于基于字符操作的通用的字符串处理。当声明一个string变量时,例如:

5.5 模板编程中的习语 - 图6

事实上,正在声明的s格式如下所示(由于在basic_string特化中有默认的模板参数):

5.5 模板编程中的习语 - 图7

由于字符特征已经从basic_string类模板中分离出来,可以使用一个惯用的特征类来取代std:char_traits。下面的例子显示了这种灵活性:

5.5 模板编程中的习语 - 图8

5.5 模板编程中的习语 - 图9

在这个程序中,为招待作为客人的类Boy和类Bear的实例,提供了适合他们口味的食物。

Boy喜欢牛奶和小甜点,Bear喜欢浓缩的牛奶和蜂蜜。客人与食物之间的关联是通过一个基本的(空的)特征类模板的特化完成的。BearCorner的默认参数保证了客人能够获得恰当的食物,但也可以用一个简单的符合特征类需求的类来代替它,就像上面用到的MixedUpTraits类。这个程序的输出是:

5.5 模板编程中的习语 - 图10

特征类的使用提供了两个关键的优点:(1)在将对象与其关联的属性或函数配对方面提供了灵活性和可扩充性;(2)它保持了模板参数列表的短小易读。如果一个客人与30个类型相关,那么,将所有30个参数直接在每一个BearCorner声明中指定,这将是非常不方便的。而将这些类型放在一个独立的特征类中就会大大简化这项工作。

如第4章所述,特征技术也可用于实现流和区域化。在第6章有一个名为PrintSequence.h头文件,其中可以找到一个迭代器特征类的例子。

5.5.2 策略

如果检查一下用wchar_t特化的char_traits,就会发现实际上它相当于char特化的副本:

5.5 模板编程中的习语 - 图11

两个版本惟一的真正的区别是,所包含的类型集不同(char和int分别相对于wchar_t和wint_t)。两者所提供的函数是相同的。[2]这更突出了一个事实:特征类是为特征(trait)而设计的,在相关的特征类之间的改变通常就是类型和常量值,或者是使用了相关类型的模板参数的固定算法。通常特征类本身就是模板,因为它们包含的类型和常量通常被看做是基本模板的特征参数(例如,char和wchar_t)。

将函数(functionality)与模板参数关联起来也是有用的,因而客户端程序员在他们编码的时候能够轻松地定制代码行为。举例来说,下面的这个BearCorner程序版本,支持不同的招待类型:

5.5 模板编程中的习语 - 图12

BearCorner类中的Action模板参数希望有一个名为doAction()的静态成员函数,它用在BearCorner<>:entertain()中。用户按照意愿可以选择Feed或Stuff,二者都提供了所需的函数。用这种方式来封装函数的类称为策略类(policy class)。在上例中,招待“策略”是通过Feed:doAction()和Stuff:doAction()提供的。这些策略类可能是普通类,也可能是模板,还有可能是结合了使用继承机制全部优点的类。关于基于策略的更深入的设计技术,请参看Andrei Alexandrescu的书[3]。关于这个主题,这本书具有权威性。

5.5.3 奇特的递归模板模式

任何一个初学C++的程序设计者都知道如何修改一个类,使它跟踪一个类当前实际存在的对象个数。必须做的所有工作就是添加静态成员、修改构造函数和析构函数的逻辑,如下所示:

5.5 模板编程中的习语 - 图13

CountedClass的所有构造函数都对静态数据成员count进行增1计数,而析构函数进行减1计数。静态成员函数getCount()获取当前对象的个数。

每次想为新添加的一个类的对象进行计数的时候,手工添加这些成员实在是太枯燥了。在面向对象程序设计中,过去常常对代码进行重用或共享采用的是继承方式,在本例中这也只是半个解决方案。请观察,当在基类中使用计数逻辑时会有什么情况发生:

5.5 模板编程中的习语 - 图14

5.5 模板编程中的习语 - 图15

派生自Counted的所有类都共享了相同的、惟一的静态数据成员,因此通过跨越Counted层次结构中所有的类,它们的对象个数全部被跟踪。现在所需的是,有一种能自动为每个派生类生成一个不同基类的方式。一种奇特的模板构造实现了这种方式,如下所示:

5.5 模板编程中的习语 - 图16

每个派生类都派生于一个惟一的基类,这个基类将它本身(派生类)作为模板参数!它看起来像是陷入了一个递归(循环)的定义,而且还有可能在某次计算中将某个任意的基类成员作为模板参数。由于Counted的数据成员不依赖于T,当模板被解析的时候,Counted的大小(为零!)就可以知道。因此究竟使用什么样的参数来实例化Counted无关紧要,因为它的大小总是相同的。当它被解析时,用任意一个Counted实例的派生类当然也可以完成,而且不会产生递归。由于每个基类都是惟一的,它有属于自己的静态数据,因而无论如何,这都是一个实现了向任意类中添加计数的便捷方法。Jim Coplien是第1个在刊物上提出这种有趣的派生方法的人;他在一篇名为“奇特的递归模板模式(curiously recurring template pattern)”[4]的文章中提出了这个方法。

[1]另一个模板用语,混入继承,将在第9章讨论。

[2]实际上(这不是实质上的。例如,)char_traits<>:compare()在一个实例中可能调用函数strcmp(),而在另一个实例中就有可能调用wcscmp()。而在这里所说的是compare()函数执行的功能是相同的。

[3]《Modern C++Design:Generic Programming and Design Patterns Applied》,Addison Wesley,2001。

[4]《C++Gems》,由Stan Lippman编辑,SIGS,1996。