序言
难道每门语言都难免日趋复杂,并最终绊倒在复杂性的门槛上吗?
—Adam Connor
难用的话,少用就是了。
—Melanie Krug
A Dichotomy of Character
三年前,《Imperfect C++(www.cppentry.com)》快要完工时,我跟编辑说起这本《Extended STL》,当时我信心满满地声称它会是一本易读易懂、且轻薄短小得可以轻松从两个抽象层之间滑过的小册子。此外我还保证会在半年之内写完。结果呢?在写这篇序言的时候,离当初约好的截稿日期已经过去了一年半有余,而且,本来计划好的一本薄薄的、约十六至二十个章节的小册子现在也膨胀成了两卷本,其中第一卷洋洋洒洒四十三个章节(含“插曲”章节),哦,对了,CD上还有三章呢…但话说回来,当初有一个保证现在仍然可以说是成立的,那就是这是一本对任何有一定C++(www.cppentry.com)经验的读者来说都轻松易懂的书。
为什么这本书后来的情况远远超出我当初的预计呢?并不是因为我是软件工程师——大家都知道软件工程师估计的工作量,乘三之后才是实际需要的时间。而是因为(我认为)以下四个重要的原因:
1.STL并不直观,花费可观的智力投资后方能熟练运用。
2.没错,STL在技术上功臻化境;没错,STL在内聚性方面超凡入圣。然而,STL前瞻性不够,对于在它那有限的概念定义之外的抽象,它并不能妥善应付。
3.C++(www.cppentry.com)语言本身是不完美的。
4.C++(www.cppentry.com)是一门难学的语言,但你得到的回报是效率,而且同时又并没有牺牲设计。
最近几年C++(www.cppentry.com)与时俱进,这一方面意味着C++(www.cppentry.com)变得非常强大,但另一方面也暴露出了其晦涩难懂的一面。如果你试着编写一个具有一定规模并用到模板元编程(www.cppentry.com)的模板库,那么一种可能是你将学到许多东西,并掌握了一个强大的工具;但同样也很可能的是你编写出的那堆东西除了C++(www.cppentry.com)狂热信徒之外谁也无法理解。
C++(www.cppentry.com)的使用精神本来就是扩展。除了很有限的一些应用是将C++(www.cppentry.com)看作“更好的C”来使用的之外,绝大多数C++(www.cppentry.com)使用都是围绕着类型定义(类、枚举、结构、联合)来进行的,而且这些自定义类型很大程度上被做成与内建类型界面一致。也正因为这个原因,C++(www.cppentry.com)中的许多内建操作符都是允许重载的。这样,一个vector,比如说,便可以重载operator[]来模拟内建数组的界面;再举个例子:任何可被拷贝(Copyable)的类型(一般)都定义(重载)了拷贝赋值操作符(operator=)。如此等等,不一而足。但由于C++(www.cppentry.com)的不完美、强大、以及极强的可扩展性,伴随而来的便是Joel Spolsky所说的抽象渗漏法则(the Law of Leaky Abstractions):“所有非平凡的抽象在某种程度上都是有漏洞的。”简单来说这句话就意味着要想顺利使用非平凡的抽象就必须对抽象下面的东西有所了解。
这也正是许多C++(www.cppentry.com)开发者重新发明轮子 的原因之一。其实这里的原因并不仅仅是众所周知的所谓“非我发明症”(NIH) ,而是因为我们常常发现自己所用的第三方组件除了百分之八十的功能是自己能理解并使用的之外,剩下的百分之二十往往裹着一团晦涩的黑气;造成后者的原因很多:复杂性、与既有概念或惯用法的不一致、低效、效率、范围局限性、设计或实现的不优雅、糟糕的编码风格等等。而且,编译技术现阶段的一些实际问题还会极大地加剧这种情况,尤其是遇到模板实例化过程中的错误消息时。
我觉得我之所以有资格写这本书,原因之一就是我花了大量的时间来研究并实现STL相关的库,而不是接受C++(www.cppentry.com)标准(1998)所指定的库或其他人写的库。而我决定写这本书的原因之一则是想将我在以上过程中学到的东西总结出来。如果你想要编写STL扩展,这本书可以为你提供帮助;而就算你只是想使用其他人写的STL扩展,这本书也同样有用,因为抽象渗漏法则决定了你很可能会不时需要掀开抽象这块幕布往里面瞧一瞧。
UNIX编程(www.cppentry.com)的原则
在《UNIX程序设计艺术》(Addison-Wesley, 2004)中,Eric Raymond总结了UNIX社群的最佳实践准则,这些准则来自大量不同的经验。在我们改装STL的宏大计划中,这些准则就像标灯一样为我们指明方向:
清晰原则:清晰比巧妙重要。
组合原则:设计能够互相连接的组件。
多样性原则:质疑任何被声称为“真正唯一”的途径。
经济原则:程序员时间是昂贵的;在它跟机器时间之间,优先节省前者。
可扩展性原则:在未来时态下设计,因为未来比你想像得来得更快。
生成原则:避免手动编码;可以的话,编写程序来生成程序。
最小意外原则:在接口设计中作出的决策应该始终是那个令人最少感到意外的选择。
模块性原则:编写简单的模块,模块与模块间通过干净的接口连接。
最大意外原则:如果免不了要失败的话,要弄出最大动静,而且失败得越早越好。
优化原则:首先要能工作,然后才能谈得上优化。
Principle of Parsimony: Write large components only when it is clear by demonstration that nothing else will do.
吝啬原则:除非能够明确证明别无它法,否则不要编写大的组件。
健壮性原则:透明性和简单性是健壮性的父母。
分离原则:策略和机制分离;接口与引擎分离。
简单原则:设计应该是简单的;只在必须的时候才增加或暴露复杂性。
透明原则:设计的时候应考虑透明性,以方便检查和调试。
优秀的C++(www.cppentry.com)库的七个标志
除了以上原则之外,本书(及其第二卷)中的内容是基于优秀C++(www.cppentry.com)库的七个标志来进行选择的,这七个标志为:效率、可发现性、透明性、表达力、健壮性、灵活性、模块性、以及可移植性。
效率
当年,我刚走出大学门的时候,身负四年C、Modula-2、Prolog以及SQL编程(www.cppentry.com)经验(本科阶段)外加三年用C++(www.cppentry.com)写光网络模拟器的经验(研究生阶段),一时间以为自己牛得不行。而且更糟的是我觉得任何使用C/C++(www.cppentry.com)之外的语言的程序员都是无知、没脑子、不成熟的。我当初的这种认识并不像有些人可能认为的那样错在没认识到其实自己还有十二年(还没结束呢)才能对C++(www.cppentry.com)初窥门径,而是没有认识到其它语言的优点。
如今我理智了许多,我意识到软件工程是一块广大的领域,其中有着许多不同的需求。执行时间并非唯一重要的因素,更不用说还有凌驾于它之上的“经济原则”了。比如说编写系统脚本时,用C++(www.cppentry.com)要花三天才能写完(只为了获得十分之一的性能提升)的程序使用Python或(复杂一点的)Perl或(最好是)Ruby可能只要三十分钟就搞定了。许多时候,当同一数量级上的性能差异无伤大雅时,选择Python/Perl/Ruby要明智得多。以前我觉得C++(www.cppentry.com)才是王道,现在呢,我在许多任务中都选用Ruby;后者不会让你抓狂到掉头发。不过,话说回来,当需要编写的是健壮的高性能软件时,C++(www.cppentry.com)仍然是我的首选。
如果你不需要也不想编写高效代码的话,那就别用C++(www.cppentry.com),也别读本书。
(不过你还是应该买下它!)
许多人会认为选择C++(www.cppentry.com)还有其它原因,尤其是const正确性、强编译期类型安全、语言设施对实现用户自定义类型的上乘支持、泛型编程(www.cppentry.com)等等。我承认,这些的确很重要,而且许多其它语言也的确缺乏这些特性,但是当权衡了一般软件开发中的所有考虑(尤其是组合原则、经济原则、最小意外原则、透明原则和优化原则)之后,(至少对我来说)很明显效率才是那个不可或缺的考虑。C++(www.cppentry.com)的许多顽固的反对者仍然声称C++(www.cppentry.com)的效率并不高。对于这些迷失的灵魂,我只能说,如果你的C++(www.cppentry.com)程序运行缓慢,那只能是因为你没有正确使用它。更多的人会争辩说他们也可以用其它语言写出高效的程序;我承认在少数特定的应用领域内的确如此,但如果有人认为有任何其它语言可以在效率和应用范畴方面和C++(www.cppentry.com)抗衡的话,那他就大错特错了。
你可能会问,那我们为什么非得要效率不可呢?对此坊间流传的说法是,除非材料科学取得突破性进展,否则电子基板上的性能提升的数量级终将到顶。我没上过什么“谦卑预言学校”,因此在明确的证据出现之前我会对这个说法保留一点怀疑。不过就算我们在非量子的基板上进一步榨取出“剩余价值”来,有一点还是基本确凿无疑的,那就是(由于非技术的原因)操作系统还会变得越来越庞大和缓慢,而软件的复杂性也会继续水涨船高;同时软件也会运行在越来越多样化的硬件设施上。效率很重要,并且效率可能一直都会重要下去。
那么这种对效率的强调与优化原则相不相悖呢?咋一看可能的确如此;然而你得注意,库的用户群通常是很广泛的,而且库的生命期通常也很长,这就意味着总会有一部分用户对性能是有需求的。因此,一个成功的库除了要保证正确性之外,通常还要考虑效率。
STL的设计理念之一就是要非常高效,因而如果正确使用它的话效率的确会很高,本书后面就会有很多这样的例子。STL之所以高效,部分原因是因为它对扩展是开放的。但另一方面,STL也非常容易被不当使用(扩展),这时便会导致低效的代码。因此,本书的重点之一便是要倡导C++(www.cppentry.com)库开发中的效率友好的实践方式,我觉得这一点无可非议。
可发现性与透明性
虽说效率是使用C++(www.cppentry.com)的一个很大的动因,但在另外两个因素,透明性和可发现性面前它往往会略失风采,因为几乎任何你考虑使用的库在透明性和可发现性方面都有缺陷。Raymond的《UNIX编程(www.cppentry.com)艺术》中对这两个软件的一般性质作了详细的定义。不过本书中我就用自己的有关定义了,因为它们更贴近C++(www.cppentry.com)(和C)库:
定义
可发现性是指要想使用一个组件需要先花上多大工夫来理解它。
定义
透明性是指要想修改一个软件需要先花上多大工夫来理解它。
可发现性主要是指一个组件的接口有多明了(即有多“一望便知”),这里所说的接口包括形式、一致性、正交性、命名惯例、参数相关性、方法命名、惯用法的使用等等。除此之外可发现性还包括了文档、指南、范例等——只要是能帮助读者领悟到组件怎么用的资料或信息都算。所谓可发现(discoverable)接口是指容易被正确地使用,且不容易被错误地使用的接口。而透明性则更多关注于代码——文件布局、括号的使用、局部变量名字、(有用的)注释等等;不过除此之外它也包括实现文档(如果有的话)。可发现性和透明性通过两层不同的抽象层关联起来:如果一个组件的可发现性不佳,那么使用该组件的代码便会在透明性上打折扣。
另外我还想告诉你一个个人经验:在我的专业生涯中,真正被我在商业代码中大量使用的非专有C++(www.cppentry.com)库可谓少之又少(而且就连这寥寥可数的几个库在灵活性方面也仍然大有欠缺)。那要用C++(www.cppentry.com)来完成任务怎么办呢?自己写库。这话听上去很容易让人觉得我是个典型的“非我发明症”患者。然而事实其实远非如此,在其它语言中我使用了大量的第三方库:我用过几十个C库(或者具有C API的库),而且感觉很不错。而在其它语言如D、.NET、Java、Perl、Python、Ruby中,我更是会毫不犹豫地使用第三方库。那你要问了,为什么就C++(www.cppentry.com)中不行呢?
答案跟效率有点关系,但更主要的还是因为可发现性和透明性。(而且这还姑且不说运行时基于多态的面向对象根本名不符实这一条。不过关于这一点的讨论不在本书的讨论范畴之内,而且也有点超出了我的专长领域和兴趣。)
好的软件工程师对最佳方案有一种直觉,其中之一便是对使用一个组件的代价和好处具有长期磨练出来的感觉。如果一个组件在性能和/或特性方面有不少保证,但在可发现性方面却不容乐观的话,这种感觉便出现了,你内心的声音告诉你别使用它。而这也正是“自己动手,丰衣足食”的时候了。
进一步说,就算可发现性还不错(甚至非常好),那也并不意味着该组件就是佳选,因为它在透明性方面仍可能不行。透明性也很重要。简单明了的接口固然很好,但如果实现一团乱麻的话,想要修改或改进也是无从下手。其次,如果权衡到最后你还是倾向于使用一个组件而不管它的低可发现性的话,则你可能会需要“偷窥”它内部的实现来弄明白究竟如何用它。第三,你可能会想要知道如何实现一个类似的库,这也正是开源时代的库的重要角色之一。最后一点,有一个常识是,如果实现糟糕,那么其它方面也未必好到哪儿去。
对我来说可发现性和透明性是C++(www.cppentry.com)库的两个极重要的性质,这一点不仅在本书中被重点强调,在我的代码中也如此:在我写的库当中你会看到清楚精确的(有人可能会觉得是“学究式”的)代码结构和文件布局。当然,我并不是说所有代码都是透明的,“但是我在努力,Ringo,我真的很努力” 。
表达力
人们使用C++(www.cppentry.com)的另一个原因便是C++(www.cppentry.com)具有极强的表达力(强大)。
定义
表达力是指一项给定的任务多大程度上能清楚地用尽可能少的代码表达出来。
表达力强的代码有三大好处。一,提供更高的生产率,这是因为不仅需要编写的代码变少了,而且代码的抽象层次也更高。二,促进了代码复用,进而带来了更好的健壮性,因为被复用的组件的实现在它们被使用的上下文中得到了更多的覆盖。三,使代码更少bug。Bug的数量与代码行数很大程度上是有一定关系的,此外,当代码工作在高抽象层面上时,控制流问题和显式内存管理的减少也会一定程度上减小bug发生的机会。
举个具体的例子,以下C代码片段的功能是删除当前目录下的所有文件:
DIR* dir = opendir("."); if(NULL != dir) { struct dirent* de; for(; NULL != (de = readdir(dir)); ) { struct stat st; if( 0 == stat(de->d_name, &st) && S_IFREG == (st.st_mode & S_IFMT)) { remove(de->d_name); } } closedir(dir); } |
我猜大多数合格的C程序员只要看一看就能知道以上代码是干什么的。比如说吧,假设你是一个有经验的VMS/Windows程序员,而你最先接触到的UNIX代码就是以上这段代码。我敢打赌你立即就能领会代码的意思,唯一可能不理解的地方只有S_IFREG和S_IFMT这两个文件属性常量。这段代码透明性很高,这意味着opendir/readdir API的可发现性很高,后者的确是事实。然而另一方面这段代码的表达力却不算很强,它很冗长,而且里面包含了许多显式的控制流,每次你想要完成类似的任务时都得重复这份逻辑并加上一点自己的小改动,经典的拷贝——粘贴,真是活受罪。
现在让我们来看看C++(www.cppentry.com)/STL式的做法,在我的STLSoft的UNIXSoft子项目中有这样一个类:unixstl::readdir_sequence(第十九章),其用法如下:
readdir_sequence entries(".", readdir_sequence::files);std::for_each(entries.begin(), entries.end(), ::remove); |
与前面的C范例不同的是,以上这段代码不多不少,不肥不瘦,每一个单词都适得其所。对任何熟悉STL中的迭代器对(区间)和算法惯用法的人来说,这段代码的可发现性是非常之高的。其第二行相当于说:“对[entries.begin(), entries.end())这个区间内的每个(for_each)元素,将它移除(remove())掉。”这里用到了一个半开半闭区间来描述从entries.begin()到entries.end()(不含entries.end())之间的所有元素。就算没有readdir_sequence类的任何文档或先验知识,只要你知道或能猜到readdir可能意味着什么的话,这段代码的含义都是极其显然的,这便意味着透明性,而透明性则进一步意味着readdir_sequence的可发现性。
不过,高阶表达力也有它的缺点。首先,过多的抽象是透明性的敌人(从而有可能进而影响可发现性)。这就是为什么我们应该把抽象层次相对放低的原因:抽象太多的话,系统整个的透明性便会打折扣(即便在某个给定层次上的抽象是好的)。其次,由于一小段代码背后往往做了大量的工作,性能方面可能便会有所受损:一般来说,抽象下层的代码的效率也很重要。第三,组件的用户被限制在抽象层所暴露出来的特性上。在我们的例子中,readdir_sequence提供了flags来让你过滤文件还是目录,但并没有让你基于文件属性或文件大小来进行过滤。高层抽象可能会提供一些看似随便选择的功能,这可能会带来问题(毕竟,每个人的需求都不尽相同!)。
此外对于STL组件来说还有另外两个问题。一是许多STL库,包括好几个标准库实现,在可读性方面都很差,这便对透明性产生了负面的影响,使得运行期bug很难调试;而编译期bug的情况也好不到哪儿去,甚至更糟。即便是在最新的C++(www.cppentry.com)编译器上,模板实例化过程中的错误也难以卒读,这就意味着一旦代码有问题,透明性和可发现性就会极度下降 。甚至就算你的代码很简单,编译器给出的错误消息也仍可能令人丈二和尚摸不着头脑 。因而,编写STL扩展库的一个重要方面便是要预料到这些晦涩难懂的编译错误,并相应地在代码里面加入一些非功能性的代码来缓解它们,以免用户在遇到这些情况的时候被吓着。后面讲到组件的实现的时候你会看到一些例子。
第二个问题是,像上面那样的因表达力而明显获益的例子在STL中并非总是成立。你往往发现你想要的函数在库里面并没有提供,于是要么你自己写一个自定义的函数类(仿函数),结果降低表达力(因为你必须跳出当前作用域去编写单独的函数类);要么呢,你就得求助于函数适配器,后者则会降低代码的可发现性和透明性。本书的第二卷会介绍一些对付这类情况的技术,但没有哪个在效率、可发现性、透明性、灵活性和可移植性方面全都是完美无缺的。
健壮性
代码再漂亮,不能工作那也是白搭。C++(www.cppentry.com)时常被讨伐的原因之一便是人们觉得它太容易纵容糟糕的实践方式了。当然,我是不赞同这种观点的,而且我还要说,只要适度的注意,C++(www.cppentry.com)程序可以是极其健壮的。(我在现实中最爱的付薪工作便是编写运行若干年不出错的网络服务器。当然,这项工作的缺点便是维护工作的钱就赚不到了,因为根本没机会去维护。在澳洲,我写的软件携带数十亿的事务穿梭于大陆之间,稳健无比。而我从未有机会通过修正软件的bug来赚点小钱,唉,没办法,难道本来不就该如此吗?:))
而模板的使用则导致了需要更多的手段才能确保软件的健壮性,因为编译器仅当实例化一个模板的时候才真正完成类型检查。因而模板库中的bug便可能会在很长一段时间都不被编译器检查出来,直到被某个特定的实例化触发。要想缓解这种情况,C++(www.cppentry.com)库,尤其是模板库,便需要大量运用契约式编程(www.cppentry.com)中的enforcements(第七章)来侦测无效状态,以及运用约束(第八章)来防止编译器进行不该进行的实例化。
清晰原则、组合原则、模块性原则、分离原则都与健壮性密不可分;健壮性是本书重点讨论的内容之一。
灵活性
最小意外原则和组合原则是说组件的工作方式应该符合用户的预期。而模板技术刚好对此有不俗的贡献,运用模板技术,我们可以定义适用于任何类型的功能片段。一个经典的例子就是std::max()函数模板:
template <typename T> T max(T const& t1, T const& t2);
|
该函数提供的这种通用性既容易实现又容易理解。不过要想故意刁难它也不难:
int i1 = 1; long l1 = 11; max(i1, l1); // Compile error!
|
这是小的。有的问题则更严重一点。比如,你想用某个类来加载一个动态库(譬如这个类叫dynamic_library),动态库的路径被你放在一个C风格串内,如下:
char const* pathName = . . . dynamic_library dl(pathName); |
现在,假设你要换用std::string来存放路径串了,那你就要修改两行代码,尽管从逻辑上你做的还是原来的操作:
std::string const& pathName = . . . dynamic_library dl(pathName.c_str()); |
这就违反了组合原则。dynamic_library类应该能够同时应付C风格串和std::string对象。不满足这一点便会带来不必要的麻烦,而且也会导致混乱的、不利于修改的代码。
模块性
模块性不好往往导致膨胀且脆弱的单片式框架,结果就是令用户感到不愉快、性能和健壮性糟糕、表达力和灵活性受损(还没提到编译时间问题呢!)。由于C++(www.cppentry.com)使用了静态类型检查并沿袭了C的声明/包含模型,因此我们经常会发现代码中存在不恰当的耦合。尽管C/C++(www.cppentry.com)支持类型前导声明,但也只有当你使用的是类型的指针或引用时才能这么干。况且后者跟C++(www.cppentry.com)所倡导的值语义还有点相左。
模板库,如果运用得当的话,在模块性方面绝对是一柄利刃。由于编译器基于结构一致性(10.1节)来判断特定的模板实例是否合法,因此我们不需要知道一段代码所能应付的所有具体类型,更不需要通过事先包含来让它们可见,只需定下这些类型所必须遵从的concept即可,任何符合该concept的类型便都可以和我们的代码合作无间。STL便是这方面的绝佳范例,其它成功的C++(www.cppentry.com)库莫不如此。
可移植性
除非你深信目前你的库被使用的上下文(架构、操作系统、编译器、编译器设定、标准/第三方库等等)将长期有效,否则作为一个成功的库的作者,你就必须考虑可移植性。这个逻辑几乎是没有例外的,这便意味着在实践当中几乎所有的库作者都必须花精力来避免他们的库被遗弃。
你只需扫一眼你机器上的系统头文件就会发现可移植性问题是怎么回事了。但可移植性并不像你想像得那么简单,否则的话一大堆聪明的程序员也就不会老在上面栽跟头了。编写可移植的代码要求对你所依赖的假设始终有一个清楚的了解。所谓依赖的假设范围则很宽泛,比较明显的像硬件架构和操作系统;微妙一些的如库的版本,甚至库中的bug以及你为这些bug设置的workarounds。
可移植性问题的另一个来源就是C++(www.cppentry.com)方言。大多数编译器都提供选项来关掉一些C++(www.cppentry.com)语言特性,这其实就相当于提供了一门C++(www.cppentry.com)方言(或语言子集)。例如一些很小的组件经常是在关掉了异常支持的情况下构建的。只要小心行事(并花些功夫),这类情况下的可移植性是可以解决的,后面讲到的一些组件就是例子。
STL扩展从本质上需要高度的可移植性,从应付不同的操作系统差异,到应付编译器bug和语言方言;这也是可移植性在本书中受到重点对待的原因之一。
权衡:适应性满足、方言化、新的和老的惯用法极少有库能够在前面提到的七个方面都能得高分,这也在情理之中。只有很小规模且功能很紧凑的库才可能做到这一点。而对于其它库而言要做到这一点则要求在七个因素之间进行恰当的权衡。而本节的内容就是我在作出成功的权衡中用到的策略。
对于重实践轻教条的人来说,大多数问题都没有一个单一而清晰的答案;我们现在面临的这个问题同样如此。STL的强大毋庸置疑,但它同样也是晦涩的,并且使用STL的人一不小心便会写出无法维护、低效、不可移植或干脆让人无法理解的代码。在你编写自己的C++(www.cppentry.com)模板库时,可用的技术有多种,于是你一不小心便会掉进自己的风格和技术,建立只属于自己的私有惯用法。这便是所谓的方言化了,方言化会妨碍软件工程师们之间的交流。
另一方面,从库使用者的角度来说,我们也经常会发现需要克服这类方言化行为甚至干脆是糟糕的设计所带来的(可发现性上的)理解困难,并逐渐对使用的库建立起一个让人感到比较舒服的认识。这时你便落进了一个“效用局部最大化”陷阱当中。一方面,有可能你正使用的库的确是最适合你手头的任务的,并且你也最有效地运用了你的库,但同样可能的是存在另一个库比现在这个有效得多,或者说,现在这个库换种用法会有效得多。你的这种状况便被称为“适应性满足”,即对目前能工作的方案感到满意。适应性满足会导致方言化或/和对好的惯用法的忽视。但由于我们软件工程师并没有无限的时间去寻求给定问题的最佳方案,再加上我们使用的软件工具也在日益变得更庞大和复杂,因此适应性满足几乎是无可避免的。
其实我们每个人都只在复杂性的海洋中舀取了一瓢水,在这个小水洼中我们作为聪明的人类迅速适应并感到舒适。而一旦感到舒适了,我们的工作便不再那么复杂和令人不快,于是便可能逐渐被那些喜欢表达力、效率和灵活性,排斥耦合、低可移植性、低可发现性的人们接纳。最终我们的工作到底传播得多广泛则取决于市场和技术价值。然而无论如何,一旦传播得够广,我们的代码便会被完全接受,人们便开始觉得它正常而简单了,虽然事实上可能未必如此。对此最好的例子也许就是STL本身了。
不管是作为库的作者、用户,还是作者兼用户,我们都需要一些手段来帮助我们避免方言化和适应性满足。这些手段包括惯用法、反惯用法、以及窥视黑盒内部。有经验的程序员往往会忘记某个惯用法其实并不是天生就符合直觉的。比如英语中的许多单词的拼写就远远不合直觉,只是我们适应了它们而已。使用鼠标来点击按钮、菜单项和滚动栏也并不直观。同样,整个STL以及C++(www.cppentry.com)的大部分内容也都并不是天生就符合直觉的。
例如,C++(www.cppentry.com)中的类缺省情况下都是可拷贝的(指可拷贝构造加上可赋值)。其实我(以及其它一些人)觉得这一语义是有问题的。而且,防止一个类被拷贝的手法从任何意义上来讲都并不显见:
class NonCopyable { public: // Member Types typedef NonCopyable class_type; . . . private: // Not to be implemented NonCopyable(class_type const&); class_type& operator =(class_type const&); }; |
这是个被广泛认可的做法,广泛到几乎可以说是C++(www.cppentry.com)常识了。于是它就成了惯用法。类似的,STL也是一个巨大的惯用法。一旦你对STL有了一定的经验,使用标准库提供的基本STL便习惯成自然了。当STL扩展引入新的概念和实践方式时,便需要新的惯用法来“驯服”它们。
既然有了有效的惯用法,不管是新的还是老的,便会有“反惯用法”。STL实践者必须对此警惕。例如,将一个迭代器看成指针便是一个反惯用法,你很可能会被它反咬一口。
指出并加强既有的惯用法、描述有用的新惯用法、警惕假冒的/反惯用法、窥视黑盒内部,这些便是本书(及其第二卷)所要采用的战术手法,利用这些手法,我们在成功C++(www.cppentry.com)库的七个特征之间闪腾挪跃。
例库
我是个比较务实的人,所以我喜欢的一般都是些通过实际经验传授知识的书。(跟我一上来就直奔抽象,我肯定头大。)因此,书中大部分例子都来源于我自己的工作,特别是其中的几个开源项目。有人可能会觉得我这么说是在骗人,其实真实目的是想给自己的库做广告。我得承认,我的确有这么个动机,但这远非我的主要目的。真正的原因是,使用我自己的工作,我便心中有数,确切地知道自己在说什么,而后者正是在写书时极端重要的一条;此外,讨论自己工作中的错误和问题不会冒犯任何人或招来官司。而且,如若不信我上面所说的话,你完全可以下载我的库自己看看。
STLSoft
STLSoft是我的得意之作,在过去的大约五年里逐渐进入C++(www.cppentry.com)社群。STLSoft不仅完全免费,而且还是可移植的(主要是编译器间可移植,但有些地方在操作系统间也是可移植的),易用的。最后,也是最重要的是,它的效率很高。此外,像我所有的开源库一样,它使用的也是修改版的BSD许可证。
STLSoft的易用性和易扩展性来源于它的两个特点。一是它是由100%的头文件构成的。你只要包含正确的头文件就行了(别忘了将STLSoft添加到你的开发环境的包含路径中)。二是STLSoft中的组件的抽象层次被有意地放低(吝啬原则),并尽量避免混用技术和操作系统特性(简单原则)。事实上,STLSoft分为多个子项目,各自针对特定的技术领域。
STLSoft提供的许多特性都支持类似STL和非STL的编程(www.cppentry.com)风格,但其实其主要目的还是在一个相对较低的抽象层上提供通用的组件和设施,以支持商业项目和其它开源库。总的来说,STLSoft是一个高效的,高灵活性的,高可移植性的,以及低耦合性的库。当必须进行折衷时,我选择的是牺牲一些表达力和抽象丰富性来成全以上这些性质。
STLSoft子项目
有意思的是,STLSoft中最主要的子项目也叫STLSoft,其中包含了大部分平台和技术无关的代码;例如分配器和分配器适配器(见卷2),算法(见卷2),迭代器和迭代器适配器(第三部分),内存用具类(utilities),字符串操作函数和类(第27章),用于定义(时间和空间上都很高效的)C++(www.cppentry.com)类属性(properties)的类(见Imperfect C++(www.cppentry.com)第35章),元编程(www.cppentry.com)组件(第12,13和41章),垫片(第9章),编译器和标准库特性甄别(和替换),等等。STLSoft子项目中的所有组件都位于sltsoft名字空间内。
另外三个最大的子项目是COMSTL,UNIXSTL,和WinSTL,其组件分别位于comstl,unixstl,和winstl名字空间内。COMSTL提供了一大堆实用组件,用于COM(Component Object Model 组件对象模型)编程(www.cppentry.com),此外还在COM枚举器和COM集合的概念之上,提供了STL兼容的序列适配器;这些分别在第28和30章中介绍。此外COMSTL也支持我的一个新库,VOLE(也是100%头文件,书附CD上有),VOLE是一个健壮、简洁,编译器无关的库,用于用C++(www.cppentry.com)来驱动COM自动化服务器。
UNIXSTL和WinSTL子库则分别为UNIX和Windows提供了操作系统相关和技术领域相关的一些组件,其中有几个在后面第一和第二部分将会提到。这两个子库中有许多结构一致的组件,如environment_variable,file_path_buffer(16.4节),filesystem_traits(16.3节),memory_mapped_file,module,path,performance_counter,process_mutex,和thread_mutex。它们以及一些全新的组件,如environment_map(第25章),被一并放在platformstl名字空间中,构成了PlatformSTL子项目。注意,这跟抽象掉操作系统间差异性的做法有很大的区别:只有那些结构上足够一致,使用中感觉不到平台相关性,并不必引入大量预处理来对付平台差异性的组件才允许进入PlatformSTL。
其余子项目则分别针对另一些技术相关的领域。比如ACESTL将STL概念应用到一些ACE(Adaptive Communications Environment自适应通信环境)组件上(第31章)。MFCSTL力图将老旧的MFC(Microsoft Foundation Classes微软基础类)向STL风格靠拢(比如第24章我们介绍的一个CArray适配器,它跟std::vector就很相近)。RangeLib是STLSoft实现的区间概念;“区间”在第2卷中介绍。此外还有ATLSTL,InetSTL和WTLSTL这些小巧的项目,分别对ATL,网络编程(www.cppentry.com),和WTL进行STL式的增强。
尽管每个STLSoft子项目似乎都有一个属于自己的单独的顶层名字空间(除了STLSoft这个主项目,它位于stlsoft名字空间中),但实际上这些名字只是stlsoft名字空间中相应的嵌套名字空间的别名。例如,comstl名字空间实际上定义于COMSTL的根头文件<comstl/comstl.h>中,如下:
// comstl/comstl.h namespace stlsoft { namespace comstl_project { . . . // COMSTL components } // namespace comstl_project } // namespace stlsoft
namespace comstl = ::stlsoft::comstl_project; |
其它所有COMSTL组件分别定义于相应的COMSTL头文件中(这些头文件都包含<comstl/comstl.h>),但它们的组件也都定义在stlsoft::comstl_project名字空间中,从客户代码的角度看它们跟位于comstl名字空间中没有区别。那么为什么要费这个事呢?因为这么一来的话,stlsoft名字空间中的所有组件对于comstl名字空间中的组件来说便都成了可见的了,于是便省去了大量的打字和干扰视线的名字空间修饰。其它所有子项目也都采用了这种做法。
小技巧
利用名字空间别名,你就既能使用名字空间层次结构,又能同时尽可能不影响到客户代码。
Boost
Boost是一个开源组织,它的目的是开发与标准库相集成,并可能被提议作为标准库候选的库。Boost库的贡献者众多,其中包括好几个C++(www.cppentry.com)标准委员会成员。
我不是Boost用户或贡献者,所以这本卷1不打算详细介绍Boost组件;只在27.5.4节讨论了boost::tokenizer。如果你想学习如何使用Boost,建议你去看看我朋友Bj rn Karlsson写的《Beyond the C++(www.cppentry.com) Standard Library: An Introduction to Boost》(Addison-Wesley, 2005)。
Open-RJ
Open-RJ 是一个针对Record-JAR格式的结构化文件读取器。它包含到多个语言和技术的映射,包括COM,D,.NET,Python,和Ruby。在第32章我借助于Open-RJ/C++(www.cppentry.com) Record类展示了一个通用的,用于在C++(www.cppentry.com)中模拟像Python和Ruby中那样灵活的下标操作符的机制。
Pantheios
Pantheios是一个C++(www.cppentry.com)日志库,其特点是100%类型安全、通用、可扩展、线程安全、原子,且极端高效:你不用为你用不到的付出任何性能开销,而且就你用到的来说,只需付一次开销。Pantheios的架构分为四部分,包括核心,前端,后端,和一个应用层。应用层使用STLSoft的string访问垫片(9.3.1节)来提供无限的通用性与扩展性。Pantheios的核心负责将一个日志语句的所有组成部分集成到一个单个的字符串当中,然后分派给后端,后端可以是Pantheios自带的后端,也可以是用户自己自定义的后端。前端负责决定哪些消息严重程度应进行处理,哪些则应忽略;前端也是可自定义的。Pantheios核心的实现在第38和39章讨论,这两章同时也展示了如何利用迭代器适配器来将算法应用到自定义类型上。
recls
recls(recursive ls递归式ls)是一个多平台的递归式文件系统搜索库,用C++(www.cppentry.com)编写,并提供C API。与Open-RJ一样,recls包含到多个语言和技术的映射,包括COM,D,Java,.NET,Python,Ruby,和STL。进行recls搜索时需要指定一个搜索根目录,一个搜索模式,以及调节搜索行为的标志,见20.10, 28.2, 30.2, 34.1, 和36.5节。和Pantheios一样,它也使用了许多STLSoft组件,包括file_path_buffer,glob_sequence(第17章),和findfile_sequence(第20章)。
【责任编辑:
董书 TEL:(010)68476606】