首先说指导思想。这是一个价值观问题,我们在此提出三条标准:简单,高性能,可移植。
我们在开篇就对简单性目标作了叙述,这里再稍微展开讨论一下。我们提出的简单标准,首先是外部接口简单,其次是内部结构简单。我们知道,类库是提供给上层应用程序使用的,也就是按照一定的接口规范,向上层提供一定的功能服务。接口设计得越简单,对上层用户来说就越方便,就越不容易产生Bug。我们可以注意到,流行的成功类库都是拥有简单接口的。为了使接口简单,常常不得不把有关具体实现的复杂性封装于类库内部,也就是说,关于简单性的设计原则,外部接口简单优先于内部实现简单。
高性能是C++语言优于其它OO语言的一个特性。C++的高性能应该首先归于它运行模式,和大多数OO语言不同,C++程序编译后直接产生本地平台代码(Native Code),理论上具备了可能的最大执行性能。另外的一个原因是主流的C++编译器都被设计得非常精巧,具有优越的代码优化能力。对于C++类库设计者来说,保持C++的高性能是一个重要目标。程序的高性能可以从两方面来评价,一是时间性能,以尽量短的时间来解决尽量多的业务;二是资源性能,以尽量少的资源消耗,包括CPU使用、内存占用、网络流量、磁盘空间等等,来维持正常的程序功能。提高性能的主要手段是数据结构、算法和程序体现结构的优化设计000-861 117-102 。
再说可移植性。C++的编译后输出代码是本地平台代码,因此C++本身不具有目标代码可移植性,C++的可移植性只能是源代码可移植性。源代码的可移植性是指,同一软件产品的全部或者部分源代码可以在不同的编译环境中进行编译(不需要编译的除外),并且其结果具有相同的品质特性(依优先顺序包括功能性、可靠性、可用性、性能性、可维护性等)。编译环境可以大致分为三个层次,最底层的是操作系统,也就是平台(Platform),其次是对源代码直接进行处理的编译器,然后是其它在编译过程中必需的中间件物品,如库文件等。我们知道C++虽然在语言规范上获得了统一(ISO/IEC),其编译器却是群雄割据的局面,具有代表性的有Borland C++系列(已经淡出市场),Microsoft的Visual Studio系列的C++编译器和GNU阵营的压轴产品gc中的g++。源代码经编译环境处理后产生的可执行代码的执行平台称为目标平台,不同的编译器的目标平台也不同,有的支持多平台,如g++,有的是单一平台,如Visual C++。对于类库设计者来说,想要获得完全的可移植性是非常困难的(除非是象STL这样被纳入语言规范的类库,因为不支持STL就是不支持标准的C++。即便如此不同的编译环境还是存在不同的STL实现版本,造成“一个类库多个实现”的局面),我们只能有选择地支持一部分环境。我们在开篇就已经说明,我们选择g++和Visual C++编译器,选择Linux和Windows 32位目标平台。
接下来我们来讨论C++类库设计的方法论。
首先,我们采用仅用头文件的类库设计方式(Header-only,STL的大多数实现版本都是采用Header-only的方式),也就是在头文件(.h)中声明和定义类,将其成员函数全部定义为内联函数,而不使用源程序文件(.cpp)。
我们知道在C语言的开发环境中,所谓库文件包含两个部分,头文件部分和二进制文件部分。根据二进制文件和用户目标文件结合方式的不同,又可分为静态链接文件和动态链接文件。这种库的构成模式已成为事实上的C语言开发环境的标准,绝大多数平台、绝大多数编译器都使用这种模式 117-301 190-721 。
然而C++语言开发环境,这种库构成模式遭遇到一个重大问题,就是符号命名问题。举例来说,C++允许多个函数可以被重载(Overload),可以具有相同的名称,而通过参数列表不同被予以区别。这样就带来一个问题,编译完成的目标代码中怎样来区别这些在源代码中具有相同名称的函数?常见的做法是在编译器输出的函数的符号名称中加入描述类型信息的字符串,这种方法通常被称为名称装饰(Name decoration)或者名称糟化(Name mangling,这个术语真不好翻译,笔者的感觉是发明这个词的人觉得编译器把本来简单干净的符号给搞乱了)。比如说,g++3.4.4对于函数void func(int),其编译输出符号名称为_Z1funci,对于函数void func(int, int),其输出符号名称为_Z1funcii,等等。但是,这种名称装饰规则没有统一规范,也就是说不同的编译器有各自不同的名称装饰规则,这样就导致不同的C++编译器只能识别自己的输出文件,而没有办法处理其他编译器的输出文件。因此,如果将C++程序制作成二进制的库文件,则其能够支持的开发环境只能限于原始的开发环境,基本上不具有多种开发环境间的通用性。
一个解决办法是将库文件保持在源代码形态(包括头文件和源文件),而不编译成二进制文件。比如STL的许多实现版本都是以头文件形式存在。这样虽然解决了名称装饰所带来的不可移植问题,但同时又会带来代码编译时间增长,源代码完全公开等问题。在C++的名称装饰规则未被统一之前,看起来这个问题是很难两全其美地解决了。
在本系列中,我们也仿照g++的STL实现方式,完全以头文件形式来编写类库。为什么不把代码放到源文件中去呢?主要原因是,头文件只需要用户使用包含指令(#include)就可以处理了,而源文件则需要配置到用户工程的编译目标列表中,和用户的源程序形成共同编译的形式,破坏了用户工程的编译目标的封闭性,比较麻烦而且不符合软件开发的一般习惯。
其次我们来讨论如何支持多平台。我们已经说过在本系列中我们的线程库支持Linux平台的Posix线程和Windows 32位平台的线程模式。我们可以参考C++的Pimpl“惯语”(Pimpl idiom,在Herb Sutter的《Exceptional C++》中有介绍),采用2层类构造方式。上次类亦即接口类,为用户提供统一的类接口,在用户看来具有唯一的类行为定义;下层类亦即实现类,将接口类的行为定义转化为某个平台的具体实现。