序言
不完美主义实践者的哲学
在本书中,C++(www.cppentry.com)语言技术与良好的实践方式占有同等重要的地位。本书并非仅仅讨论在某个特定场合下什么方案才是有效的或技术上正确的,更重要的还是要看最终哪种做法才是更安全的或者更切合实际的。本书要传达的意思有4个方面:
原则1--C++(www.cppentry.com)是卓越的,但并不完美。
原则2--穿上"苦行衣"。
原则3--让编译器成为你的仆从。
原则4--永不言弃,总会有解决方案的。
这四项原则构成了我所谓的"不完美主义实践者"的哲学。
C++(www.cppentry.com)并不完美
多年以前,一位为她最小的儿子过分骄傲的心理感到不安的母亲曾这样教导说:"如果你打算将一些好的东西告诉别人,那么你最好也准备承认其中那些糟糕的成分。"谢谢你,母亲!
C++(www.cppentry.com)是一门杰出的语言。它支持高阶概念,包括基于接口的设计、泛型、多态、自描述的软件组件以及元编程(www.cppentry.com)(meta-programming)等。此外,凭借对低阶特性的支持,它在提供对计算机的精细控制方面,包括位操作、指针以及联合(union)等,也比大多数语言更有能耐。借助于这些范围宽广的能力,外加保持对高效性的根本性支持,C++(www.cppentry.com)可以说是当今最杰出的通用编程(www.cppentry.com)语言。 不过话说回来,C++(www.cppentry.com)并非完美无瑕,实际上远没到完美的程度,因而有了本书的名字--"Imperfect C++(www.cppentry.com)中文版"。
由于一些很好的原因(有些是历史原因,也有些是当前的原因),C++(www.cppentry.com)不仅是一个折衷[Stro1994]的产物,而且还是一些互不相干、有时甚至是互不兼容的概念的混合体,因而其中必然存在着一些缺陷。有些缺陷只是鸡毛蒜皮,但有些就不是那么无关紧要了。许多缺陷都是从它们的"祖辈"那里承袭而来的。其他则是由于语言将效率放在高优先级(幸好如此)才导致的。有些则可能是任何语言都无法摆脱的根本限制。正是由于如今的语言变得愈来愈复杂,也愈来愈多种多样,因而才出现了一些极有趣的问题,这些是任何人都始料未及的。
本书直面这种复杂的形势,坚信总能够克服复杂性,坚信控制权最终还是掌握在那些见多识广、经验丰富的计算机专家手中。我的目标是缓解那些使用C++(www.cppentry.com)的软件开发者日常经受的不知所措以及无法作出决断的痛苦之情。
本书致力于解决的并不是软件开发者因经验不足或知识不够而遇到的问题,而是这个职业中的所有成员,从初学者甚至到最有才干和经验的那些人,所共同遭遇的问题。这些问题中部分源于语言自身固有的不完美性,部分源于人们对语言所支持的一些概念的常见误用。无论如何,它们给我们所有人带来了麻烦。
本书并不仅仅是对语言中的不完美之处进行一些简单的论述并附带一些"别这么做"列表,你可以找到很多着眼于这方面的C++(www.cppentry.com)书籍。和它们不同,本书的重点在于如何为我所指出的缺陷(中的大部分)提供解决方案,并借此使得这门语言变得不再像它本来那么"不完美"。本书重在赋予开发者能量,讨论他们赖以谋生的手段-C++(www.cppentry.com)-中潜在的问题领域,给出了一些重要的相关信息,并为开发者提供了一些建议以及经过实践检验的技巧和技术,以帮助他们避免或应付这些问题。
苦行僧式编程(www.cppentry.com)
在我们阅读过的很多教科书中,即便是非常好的,也只是告诉我们C++(www.cppentry.com)能够提供的解决问题的手段,前提还得是你必须将C++(www.cppentry.com)中的有关特性有效地利用起来。然而,这些书常常又会在后面接着说道:"这样做其实没有实质性的意义"或者"严格来说那样做就稍微有点过头了"。不止一个以前的同事曾把我拖进类似的激烈争论之中。通常人们的理由可以归结为:"我是一名有经验的程序员,我并不会犯XYZ阻止我犯的那些错误,干嘛要为之烦神呢?"
唉!
这个论点简直不堪一驳。我也是一名有经验的程序员,但我每天至少会犯一个低级错误;假如不是养成了严格的习惯的话,则会是一天10个!他们的这种态度其实就是在假定他们的代码永远都不会被没有经验的程序员看到。此外,他们的说法要得以成立,等于是在说代码作者永远也不会学习或者改变观念、习惯以及方法论。最后一点,到底怎样才算是"有经验的程序员"呢?
上面提到的这类人不喜欢引用、常量成员、访问控制、explicit、具体类(concrete classes)、封装、不变式,他们甚至在编码时根本就无视可移植性或可维护性。但他们就是喜欢重载、重写(override)、隐式转换、C风格强制(C-style casts),并到处使用int。他们还喜欢全局变量、混合式typedef、dynamic_cast、RTTI、专有的编译器扩展以及友元。他们在编码风格上总是不一致,让事情看起来似乎比原本还要复杂。
请容许我暂时扯开话题,讲述一个历史典故。自从1162年被亨利二世封为坎特伯雷大主教之后,Thomas A Beckett就经历了一场人格上的转变,从原先物质主义的生活中改过自新,真正开始关心起贫穷的人们,并为他以前的无度行为深刻地进行了忏悔。后来人们在准备埋葬他的躯体的时候,发现他穿着一件粗糙的、爬满跳蚤的苦行衣。事后人们才知道原来他生前天天被修道士们鞭打。天哪!
我个人觉得为了忏悔和净化自己的灵魂,这种做法未免有点过分。不过,回想起当初自己滥用C++(www.cppentry.com)的强大能力干了许多糟糕的事情(见附录B),如今我尝试采用一种更有节制的做法,因此就有了"在编程(www.cppentry.com)时穿上苦行衣"这一说法。
当然,我并不是说得像头悬梁、锥刺股那般,也不是说我在编码时不再听开得震天响的舞曲;都不是,我只是说,我会尽我所能让我的软件来严厉地对待我,以便在我企图去误用它时将我打住。我喜欢const,许多const,我会在任何可能的地方使用它。我尽可能使用private,我优先使用引用(references)。我尽可能实施不变式。我将资源返还给它的出处,即便我知道有其他安全的"捷径":"呃,你知道的,它在该操作系统以前的版本上可是一点问题也没有,你更新系统那是你的问题,跟我可没关系!"我使用所谓的"概念性typedef"来增强C++(www.cppentry.com)类型检查。我使用9种编译器,并借助于一个工具(见附录C),让它们的使用变得更加简便直观。我还使用了一个更为有效的NULL。
我并不是为了获得"年度程序员"奖项提名才这么做的。这些只不过是我一直以来懒惰所导致的结果。所有优秀的工程师都有懒惰的习惯。懒惰意味着你不想在运行期查错;懒惰意味着你不想第二次犯同样的错误;懒惰意味着尽可能地榨取你的编译器的能力,这样你才得以清闲。
让编译器成为你的仆从
"batman"一词起源于大英帝国时代,意指勤务兵或个人仆从。如果你能够正确地对待编译器的话,你就可以让编译器成为你的左右手,或者说仆从(或者你的"超级英雄",随你喜欢)。
你的编程(www.cppentry.com)"苦行衣"越粗糙,你的编译器就能够为你服务得越周到。然而,有时你的编译器对语言尽心尽责的行为也会反过来妨碍你的意图,顽固地拒绝履行你知道是合理的(或者至少是你想要完成的)事情。
这本书将会为你提供一些技巧和技术,通过它们你得以从编译器那里夺回控制权,并作出最终决策:获取你想要的而不是你被给予的。这件工作并不轻松,但我们得承认,软件开发者才是开发过程中的主宰;而语言、编译器和库只不过是被我们所驱使的工具而已。
永不言败
尽管我接受的大部分教育都属于理科方面的,但实际上我更是一名工程师。我喜欢那些早期的科幻小说,其中的那些英勇的工程师总能在面临棘手的情况时找到出路。这也正是本书所采用的方式。理论是有的,而且我们一开始也是跟着理论走。但每当我们在这门语言的边缘"行走"时,我们总会发现当前大多数编译器都不是很遵从理论,因此我们必须在这一现实的约束下编程(www.cppentry.com)。正如Yogi Berra所说的,"从理论上说,理论与实践是没有任何区别的,但从实践的角度来说,它们之间区别就大了。"
这种看待问题的方式能够为我们带来强大的效果。工程师的努力(而不是理论上的归纳),再加上顽强地和C++(www.cppentry.com)中的不完美之处抗争,使我最终获得以下一系列的发现:
借助于垫片(Shims)(第20章)以及垫片所带来的类型隧道(Type Tunneling)机制(第34章)进行显式泛化(explicit generalization)。
对C++(www.cppentry.com)类型系统进行了一些扩展(第18章),使我们能够将概念上互不相干、但基于同一基类型(base type)实现的类型彼此区分开,并能够基于它们进行函数重载。
一种编译器无关的机制,提供了动态加载的C++(www.cppentry.com)对象之间的二进制兼容性(第8章)。
在C++(www.cppentry.com)规则下重现C中强大的NULL,同时不违背后者的精神(第15章)。
一个具有最大限度的"安全性"且可移植的operator bool()(第24章)。
对"封装"概念的精细分类,借此我们得以对基本的数据结构进行高效地表示和操纵,并扩充我们的"不完美工具箱"(第2章和第3章)。
一个用于高效地分配动态大小的内存块的灵活工具(第32章)。
一个能够进行快速、非侵入式的串拼接操作的机制(第25章)。
对"如何编写能够在不同的错误处理模型下工作的代码"这一问题的中肯评价(第19章)。
一个用于控制单件对象(singleton objects)的顺序性的直观机制(第11章)。
一个在时间和空间上均高效的C++(www.cppentry.com)属性(property)的实现(第35章)。
本书并不打算成为使用C++(www.cppentry.com)语言的完备指南,它只是想帮助开发者挣脱现实中的约束,以便找到应对那些不完美之处的解决方案,并鼓励以一种超越常规的思考方式来考虑问题。
当然,我本人也绝非完美,正所谓"金无足赤,人无完人"。我也干过蠢事,而且我还有点离经叛道。我有个糟糕的习惯,在应该写private的时候我会写protected。在或许应该使用IOStream(C++(www.cppentry.com)输入输出流)的时候我会偏好于使用printf()。我喜欢数组和指针,而且我还是C兼容API的忠实拥护者。我甚至并不是完全忠实地遵循苦行僧式编程(www.cppentry.com)哲学的,但我坚信这一哲学信条是实现你的目标的最可靠、最快捷的途径,当然,前提是你必须随时随地尽可能地坚持它。
"Imperfect C++(www.cppentry.com)"的精神
除了不完美主义实践者的哲学信条之外,本书大体上还反映了我在编写C++(www.cppentry.com)代码时遵循的指导原则。它们总的来说(尽管并非完全)跟"C精神"[Como-SOC]和"C++(www.cppentry.com)精神"[Como-SOP]这一对孪生法则是相吻合的。
C精神:
相信程序员:"Imperfect C++(www.cppentry.com)"不羞于作出丑陋的决策。
不要阻止程序员去做必须完成的事情:"Imperfect C++(www.cppentry.com)"实际上会帮助你完成你想做的事情。
保持解决方案的简洁:书中的大部分代码正是如此,而且是高度平台无关和可移植的。
使它快!即使影响到可移植性的保证也在所不惜:效率被给予高度的重视,尽管我们偶尔会为此牺牲可移植性。
C++(www.cppentry.com)精神
C++(www.cppentry.com)是C的一种方言,不过C++(www.cppentry.com)针对现代软件开发作了一些增强:我们在不少重要的场合下仍然依赖于和C的互操作性。
尽管C++(www.cppentry.com)是一门比C庞大得多的语言,但你并不需要为你不使用的东西付出代价(这样时间和空间的额外开销就会被保持为最小。而那些确实存在开销的地方也得被整体观察才能作出定论,因为你要比较的是等价的程序,而不是在特性X和特性Y之间进行比较)。
尽可能地在编译期捕获错误:"Imperfect C++(www.cppentry.com)"在任何适当的地方使用静态断言(static assertions)和约束(constraints)。
尽量避开预处理(大多数情况下inline、const、template等才是正道):我们会看到各种各样的技术,它们使用C++(www.cppentry.com)语言而不是预处理器来实现我们的目标。
除了这些原则外,本书还会以身作则地示范以下的做法,即在任何可能的情况下:
编写不依赖于特定的编译器(扩展和特性)、操作系统、错误处理模型、线程模型以及字符编码策略的代码;
在不可能使用编译期错误侦测的情况下采用契约式设计(Design-by-Contract)(见1.3节)。
编码风格
为了使得本书的篇幅不至于过长,我不得不在示例代码中大幅略去我惯常采用的严格编码风格(当然,有些人可能认为那是"学究气"的编码风格)。第17章描述了我在布局类定义代码时通常遵循的原则。其他编码风格,如大括号和间隔缩进风格等,就无伤大雅了。如果你有兴趣,你可以很容易地从随书光盘中找到这方面的大量材料。
术语
计算机语言就是二进制语言,而人类的语言则是各种模糊的、不精确的语言。这里我给出一些术语的定义,本书其余部分将沿用这些术语。
客户代码:表示使用其他代码的代码,通常(但不局限于)指的是客户应用程序代码使用库代码的情形。
编译单元:来自一个源文件以及该源文件所包含(依赖)的全部头文件的全部源代码所构成的整体。
编译环境:编译环境是由编译器、库、操作系统构成的一个整体环境,我们编写的代码就是在这种环境中编译的。感谢Kernighan和Pike[Kern1999]对此的定义。
仿函数(Functor):这是一个被广泛使用的术语,其含义代表的是函数对象(Function Object)或Functional,但这个称呼并不是标准的称呼。实际上我更倾向于使用函数对象(Function Object)这一术语。但有人说服了我, 说仿函数(Functor)更好一些,因为它只有一个短短的单词,更有特色,更醒目,也更容易查找,特别是在网上查找的时候。
泛用性(Generality):我或许从来都没有弄懂"泛型(genericity)"这个词,或者说至少没有弄懂它在编程(www.cppentry.com)上下文中的含义,尽管我有时候也会把这个词拿出来用一下。我猜它的意思是"编写对一系列类型都能够起作用的模板代码,这些类型在它们被使用的方式(而不是定义)上有联系",只有在这种情况下我才会使用"泛型(genericity)"这个词。而Generality(泛用性)[Kern1999]则似乎表意更恰当一些,感觉上更适用于表达这一概念,因为我不仅关心我的代码是否能够在其他(模板参数)类型上工作,同样也关心它们能否跟其他头文件和库一起工作。
除了这些概念上的术语之外,我还使用了一些特殊的语言相关的术语。我不知道你怎么想,但我确实发现C++(www.cppentry.com)中的术语挺混乱的,因此我打算花一点时间来回顾一些定义。下面的定义来自C++(www.cppentry.com)标准,但我将它们以一种更简单的方式呈现出来,以便于我们理解,而不仅仅是我个人的理解。这些定义中有些是重叠的,虽说它们针对的是不同的概念,但都是C++(www.cppentry.com)实践者的字典里的一部分。这里的"C++(www.cppentry.com)实践者"也包括我所说的不完美主义实践者。
基本类型和复合类型
基本类型(C++(www.cppentry.com)-98: 3.9.1)包括整型(char、short、int、long(long long/__int64),以上这些类型的有符号和无符号的版本, 以及bool)、浮点型(float、double以及long double)以及void类型。
复合类型(C++(www.cppentry.com)-98: 3.9.2)基本上就是表示除基本类型之外的其他所有类型,包括:数组、函数、指针(任何类型的指针,包括指向非静态成员的指针)、引用、类、联合(unions)以及枚举。
我倾向于不使用"复合类型"这个术语,因为我觉得它的名字好像是在说它是由其他东西所"组成"的一样,但实际上对于指针和引用来说根本就不是这么一回事。
对象类型
对象类型(C++(www.cppentry.com)-98: 3.9; 9)包括任何"不是函数类型、不是引用类型、也不是void类型"的类型。我同样不喜欢使用这个术语,因为按照这种说法它的范畴并不仅仅包括"类类型(class types)的实例",可是实际上人们却往往会这么想。所以我在全书中凡涉及到这种地方时一律用"实例"这一术语。
标量类型和类类型
标量类型(Scalar types)(C++(www.cppentry.com)-98: 3.9; 10)包括"算术类型、枚举类型以及指针类型"。类类型(C++(www.cppentry.com)-98: 9)即是指以class、struct或union这3个关键字之一所声明的东西。
结构是一个由struct关键字进行定义的类类型,其成员和基类的访问控制缺省情况下均为public。联合是一个通过union关键字定义的类类型,其成员缺省情况下也是public的。类则是由class关键字定义的类类型,其成员和基类的访问控制在缺省情况下均为private。
聚合体
标准(C++(www.cppentry.com)-98: 8.5.1; 1)这样来描述聚合体(aggregate):"一个数组,或一个无用户自定义构造函数、无private或protected的非静态数据成员、无基类并且无虚函数的类"。作为一个数组或一个类类型,它意味着将多个东西聚合进一个东西中,故有"聚合体"一说。
聚合体的初始化方式可以是一对包含有初始化子句的大括号,像这样:
- struct X
- {
- int i;
- short as[2];
- } x = { 10, { 20, 30 }};
尽管聚合体通常由POD类型(见下一节)组成,但未必非得如此。在上面的代码中,X的成员变量i也可以是类类型的,只要它具有一个接受单个整型参数的非显式构造函数(non-explicit constructor)(见2.2.7小节)和一个可用的拷贝构造函数即可。
POD类型
POD意思是"plain-old-data"( C++(www.cppentry.com)-98: 1.8; 5),它是C++(www.cppentry.com)中的一个非常重要的概念,但通常会被人们误解,而且虽说它是个"小东西",却相当重要。其定义也相当糟糕。标准给出了两个线索。(C++(www.cppentry.com)-98: 3.9; 2)说:"任何完备的(complete) 的POD类型T……必须满足以下条件:将组成它的一个对象的各字节拷贝到一个字节数组中,然后再将它们重新拷贝回原先的对象所占的存储区中,此时该对象应该仍具有它原来的值。"然而在(C++(www.cppentry.com)-98: 3.9; 3)处,我们又被告知:"对于任何POD类型T,如果有两个指针分别指向不同的T对象obj1和obj2,此时如果使用memcpy()函数将obj1的值拷贝进obj2中,obj2就应该跟obj1具有相同的值。"
唔,这些说法相当华而不实,不是吗?值得庆幸的是,为了充实这个定义,C++(www.cppentry.com)标准在它776页篇幅的内容中另有若干处(准确地说,是56处)零星地提到了关于POD类型的一些信息。
在[Como-POD]中,Greg Comeau指出,大多数C++(www.cppentry.com)书籍根本不提POD,并且说"大多数C++(www.cppentry.com)书籍都不值得购买"。带着为了提高销量这个"愤世嫉俗"的企图,我打算尽量把POD描述得到位一些。这应该不会太难,因为Greg在他的文章中已经提供了POD结构的所有本质特征,那我就全当搭顺风车啦。
标准(C++(www.cppentry.com)-98: 3.9; 10)对POD类型的总体性定义如下:"标量类型、POD结构类型、POD联合类型,以上这些类型的数组,以及这些类型以const/volatile修饰的版本。"上面这个定义,除了POD结构和POD联合有待澄清外,其余都非常清楚。
POD结构(C++(www.cppentry.com)-98: 9;4)是"一个聚合体类,其任何非静态成员的类型都不能是如下任意一种:指向成员的指针、'非POD'结构、'非POD'联合,以及以上这些类型的数组或引用,同时该聚合体类不允许包含用户自定义的拷贝赋值操作符和用户自定义的析构函数。"POD联合的定义类似,只不过它是一个联合而不是结构而已。注意,聚合体类不仅可以是结构(struct),也可以是类(class)。
到目前为止一切都没问题,但POD类型到底有什么重要之处呢?呃,POD类型允许我们与C函数进行交互,它们是C与C++(www.cppentry.com)之间互通的手段,因此推而广之就成了C++(www.cppentry.com)与外部世界沟通的桥梁(见第7~9章)。因此,POD结构或POD联合的存在就允许我们"知道一个普通的结构或联合在C里面是什么样子的"[Como-POD]。POD最好的助记办法就是将它看成是一种与C兼容的类型,当然同时你也不能忘记有关POD的方方面面。
POD其他重要的方面包括:
宏offsetof()所作用于的类型应该是"一个POD结构或一个POD联合"(C++(www.cppentry.com)-98: 18.1; 5),此外用在任何其他类型(见2.3.2小节和第35章)身上都是未定义的。
POD类型可以被放在联合中。这个特性被我用来制造一个约束,该约束的作用是约束一个类型为POD类型(见1.2.4小节)。
如果一个POD类型的静态变量是以常量表达式来初始化的话,那么其初始化发生于执行流进入它所在的语句块之前,而且也是在任何(不管是POD类型还是其他类型)需要动态初始化的对象的初始化之前(见第11章)。
指向成员的指针不是POD类型,这一点跟其他任何指针类型恰恰相反。
POD结构或POD联合类型可以具有静态成员、成员typedef、嵌套类型和方法。