STL 之父访谈录
本文转载自 孟岩(mengyan) 在 2001 年翻译的 Dr.Dobb’s Journal 特约记者, 著名技术书籍作家 Al Stevens 采访 STL 创始人 Alexander Stepanov 的访谈录,原文在此。
1995 年 3 月,Dr.Dobb’s Journal 特约记者,著名技术书籍作家 Al Stevens 采访了 STL 创始人 Alexander Stepanov。这份访谈纪录是迄今为止对于 STL 发展历史的最完备介绍,侯捷先生在他的 STL 有关文章里 推荐大家阅读这篇文章。因此我将该文全文翻译如下:
Q: 您对于 generic programming 进行了长时间的研究,请就此谈谈。
A: 我开始考虑有关 GP 的问题是在 70 年代末期,当时我注意到有些算法并不依赖于数据结构的特定实现,而只是依赖于该结构的几个基本的语义属性。于是我开始研究大量不同的算法,结果发现大部分算法可以用这种方法从特定实现中抽象出来,而且效率无损。对我来说,效率是至关重要的,要是一种算法抽象在实例化会导致性能的下降,那可不够棒。
当时我认为这项研究的正确方向是创造一种编程语言。我和我的两个朋友一起开始干起来。一个是现在的纽约州立大学教授 Deepak Kapur, 另一个是伦塞里尔技术学院教授 David Musser。当时我们三个在通用电器公司研究中心工作。我们开始设计一种叫 Tecton 的语言。该语言有一种我们称为 “通用结构” 的东西,其实不过是一些形式类型和属性的集合体,人们可以用它来描述算法。例如一些数学方面的结构充许人们在其上定义一个代数操作,精化之,扩充之,做各种各样的事。
虽然有很多有趣的创意,最终该项研究没有取得任何实用成果,因为 Tecton 语言是函数型语言。我们信奉 Backus 的理念,相信自己能把编程从 von Neumann 风格中解放出来。我们不想使用副效应,这一点限制了我们的能力,因为存在大量需要使用诸如“状态”,“副效应”等观念的算法。
我在 70 年代末期在 Tecton 上面所认识到了一个有趣的问题:被广泛接受的 ADT 观念有着根本性的缺陷。人们通常认为 ADT 的特点是只暴露对象行为特征,而将实现隐藏起来。一项操作的复杂度被认为是与实现相关的属性,所以抽象的时候应予忽略。我则认识到,在考虑一个 (抽象) 操作时,复杂度 (或者至少是一般观念上的复杂度) 必须被同时考虑在内。这一点现在已经成了 GP 的核心理念之一。
例如一个抽象的栈 stack 类型,仅仅保证你 push 进去的东西可以随后被 pop 出来是不够的,同样极端重要的是,不管 stack 有多大,你的 push 操作必须能在常数时间内完成。如果我写了一个 stack, 每 push 一次就慢一点,那谁都不会用这个烂玩艺。
我们是要把实现和界面分开,但不能完全忽略复杂度。复杂度必须是,而且也确实是横陈于模块的使用者与实现者之间的不成文契约。ADT 观念的引入是为了允许软件模块相互可替换。但除非另一个模块的操作复杂度与这个模块类似,否则你肯定不愿意实现这种互换。如果我用另外一个模块替换原来的模块,并提供完全相同的接口和行为,但就是复杂度不同,那么用户肯定不高兴。就算我费尽口舌介绍那些抽象实现的优点,他肯定还是不乐意用。复杂度必须被认为是接口的一部分。
1983 年左右,我转往纽约布鲁克林技术大学任教。开始研究的是图的算法,主要的合作伙伴是现在 IBM 的 Aaron Kershenbaum。 他在图和网络算法方面是个专家,我使他相信高序 (high order) 的思想和 GP 能够应用在图的算法中。他支持我与他合作开始把这些想法用于实际的网络算法。某些图的算法太复杂了,只进行过理论分析,从来没有实现过。他企图建立一个包含有高序的通用组件的工具箱,这样某些算法就可以实现了。我决定使用 Lisp 语言的一个变种 Scheme 语言来建立这样一个工具箱。我们俩建立了一个巨大的库,展示了各种编程技术。网络算法是首要目标。不久当时还在通用电器的 David Musser 加了进来,开发了更多的组件,一个非常大的库。这个库供大学里的本科生使用,但从未商业化。在这项工作中,我了解到副效应是很重要的,不利用副效应,你根本没法进行图操作。你不能每次修改一个端点 (vertex)时都在图上兜圈子。所以,当时得到的经验是在实现通用算法时可以把高序技术和副效应结合起来。副效应不总是坏的,只有在被错误使用时才是。
1985 年夏,我回到通用电器讲授有关高序程序设计的课程。我展示了在构件复杂算法时这项技术的应用。有一个听课的人叫陈迩,当时是信息系统实验室的主任。他问我是否能用 Ada 语言实现这些技术,形成一个工业强度的库,并表示可以提供支持。我是个穷助教,所以尽管我当时对于 Ada 一无所知,我还是回答 “好的”。我跟 Dave Musser 一起建立这个 Ada 库。这是很重要的一个时期,从象 Scheme 那样的动态类型语言 (dynamically typed language) 转向 Ada 这样的强类型语言,使我认识到了强类型的重要性。谁都知道强类型有助于纠错。我则发现在Ada 的通用编程中,强类型是获取设计思想的有力工具。它不仅是查错工具,而且是思想工具。这项工作给了我对于组件空间进行正交分解的观念。我认识到,软件组件各自属于不同的类别。OOP 的狂热支持者认为一切都是对象。但我在 Ada 通用库的工作中认识到,这是不对的。二分查找就不是个对象,它是个算法。此外,我还认识到,通过将组件空间分解到几个不同的方向上,我们可以减少组件的数量,更重要的是,我们可以提供一个设计产品的概念框架。
随后,我在贝尔实验室 C++ 组中得到一份工作,专事库研究。他们问我能不能用 C++ 做类似的事。我那时还不懂 C++, 但当然,我说我行。可结果我不行,因为 1987 年时,C++ 中还没有模板,这玩艺在通用编程中是个必需品。结果只好用继承来获取通用性,那显然不理想。
直到现在 C++ 继承机制也不大用在通用编程中,我们来说说为什么。很多人想用继承实现数据结构和容器类,结果几乎全部一败涂地。C++ 的继承机制及与之相关的编程风格有着戏剧性的局限。用这种方式进行通用编程,连等于判断这类的小问题都解决不了。如果你以 X 类作为基类,设计了一个虚函数 operater==, 接受一个 X 类对象,并由 X 派生类 Y, 那么 Y 的 operator== 是在拿 Y 类对象与X 类对象做比较。以动物为例,定义 animal 类,派生 giraffe (长颈鹿) 类。定义一个成员函数 mate (), 实现与另一个哺乳动物的交配操作,返回一个 animal 对象。现在看看你的派生类 giraffe,它当然也有一个 mate () 方法,结果一个长颈鹿同一个动物交配,返回一个动物对象。这成何体统?当然,对于 C++ 程序员来说,交配函数没那么重要,可是 operator== 就很重要了。
对付这种问题,你得使用模板。用模板机制,一切如愿。
尽管没有模板,我还是搞出来一个巨大的算法库,后来成了 Unix System Laboratory Standard Component Library 的一部分。在 Bell Lab, 我从象 Andy Koenig, Bjarne Stroustrup (Andrew Koenig, 前 ISO C++ 标准化委员会主席;Bjarne Stroustrup, C++ 之父 – 译者) 这类专家身上学到很多东西。我认识到 C/C++ 的重要,它们的一些成功之处是不能被忽略的。特别是我发现指针是个好东东。我不是说空悬的指针,或是指向栈的指针。我是说指针这个一般观念。地址的观念被广泛使用着。没有指针我们就没法描述并行算法。
我们现在来探讨一下为什么说 C 是一种伟大的语言。通常人们认为 C 是编程利器并且获得如此成功,是因为 UNIX 是用 C 写的。我不同意。计算机的体系结构是长时间发展演变的结果,不是哪一个聪明的人创造的。事实上是广大程序员在解决实际问题的过程中提出的要求推动了那些天才提出这些体系。计算机经过多次进化,现在只需要处理字节地址索引的内存,线性地址空间和指针。这个进化结果是对于人们要求解决问题的自然反映。Dennis Ritchie 天才的作品 C, 正反映了演化了30 年的计算机的最小模型。C 当时并不是什么利器。但是当计算机被用来处理各种问题时,作为最小模型的 C 成了一种非常强大的语言,在各个领域解决各种问题时都非常高效。这就是 C 可移植性的奥秘,C 是所有计算机的最佳抽象模型,而且这种抽象确确实实是建立在实际的计算机,而不是假想的计算机上的。人们可以比较容易的理解 C 背后的机器模型,比理解 Ada 和 Scheme 语言背后的机器模型要容易的多。C 的成功是因为 C 做了正确的事,不是因为 AT&T 的极力鼓吹和 UNIX。
C++ 的成功是因为 Bjarne Stroustrup 以 C 为出发点来改进 C, 引入更多的编程技术,但始终保持在 C 所定义的机器模型框架之内,而不是闭门造车地自己搞出一个新的机器模型来。C 的机器模型非常简单。你拥有内存,对象保存在那里面,你又有指向连续内存空间的指针,很好理解。C++ 保留了这个模型,不过大大扩展了内存中对象的范畴,毕竟 C 的数据类型太有限了,它允许你建立新的类型结构,但不允许你定义类型方法。这限制了类型系统的能力。C++ 把 C 的机器模型扩展为真正类型系统。
1988 年我到惠普实验室从事通用库开发工作。但实际上好几年我都是在作磁盘驱动器。很有趣但跟GP 毫不相关。92 年我终于回到了 GP 领域,实验室主任 Bill Worley 建立了一个算法研究项目,由我负责。那时候 C++ 已经有模板了。我发现 Bjarne 的模板设计方案是非常天才的。在 Bell Lab 时,我参加过有关模班设计的几个早期的讨论,跟 Bjarne 吵得很凶,我认为 C++ 的模板设计应该尽可能向 Ada 的通用方案看齐。我想可能我吵得太凶了,结果 Bjarne 决定坚决拒绝我的建议。我当时就认识到在 C++ 中设置函数模板的必要性了,那时候好多人都觉得最好只有模板类。不过我觉得一个函数模板在使用之前必须先显式实例化,跟 Ada 似的。Bjarne 死活不听我的,他把函数模板设计成可以用重载机制来隐式实例化。后来这个特别的技术在我的工作中变得至关重要,我发现它容许我做很多在 Ada 中不可能的任务。非常高兴 Bjarne 当初没听我的。
Q: 您是什么时候第一次构思 STL 的,最初的目的是什么?
A: 92 年那个项目建立时由 8 个人,渐渐地人越来越少,最后剩下俩,我和李梦,而且李小姐是这个领域的新手。在她的专业研究中编译器是主要工作,不过她接受了 GP 研究的想法,并且坚信此项研究将带给软件开发一个大变化,要知道那时候有这个信念的认可是寥寥无几。没有她,我可不敢想象我能搞定 STL, 毕竟STL 标着两个人的名字:Stepanov 和 Lee。我们写了一个庞大的库,庞大的代码量,庞大的数据结构组件,函数对象,适配器类,等等。可是虽然有很多代码,却没有文档。我们的工作被认为是一个验证性项目,其目的是搞清楚到底能不能在使算法尽可能通用化的前提下仍然具有很高的效率。我们化了很多时间来比较,结果发现,我们算法不仅最通用,而且要率与手写代码一样高效,这种程序设计风格在性能上是不打折扣的!这个库在不断成长,但是很难说它是什么时候成为一个 “项目” 的。STL 的诞生是好几件事情的机缘巧合才促成的。
Q: 什么时候,什么原因促使您决定建议使 STL 成为 ANSI/ISO 标准 C++ 一部分的?
A: 1993 年夏,Andy Koenig 跑到斯坦福来讲 C++ 课,我把一些有关的材料给他看,我想他当时确实是很兴奋。他安排我 9 月到圣何塞给 C++ 标准委员会做一个演讲。我演讲的题目是 “C++ 程序设计的科学”, 讲得很理论化,要点是存在一些 C++ 的基本元素所必须遵循的,有关基本操作的原则。我举了一些例子,比如构造函数,赋值操作,相等操作。作为一种语言,C++ 没有什么限制。你可以用 operator==() 来做乘法。但是相等操作就应该是相等操作。它要有自反性,A == A; 它要有对称性,A == B 则 B == A; 它还要有传递性。作为一个数学公理,相等操作对于其他操作是基本的要素。构造函数和相等操作之间的联系就有公理性的东西在里边。你用复制构造函数生成了一个新对象,那么这个对象和原来那个就应该是相等的。C++ 是没有做强行要求,但是这是我们都必须遵守这个规则。同样的,赋值操作也必须产生相等的对象。我展示了一些基本操作的 “公理”, 还讲了一点迭代器 (iterator), 以及一些通用算法怎样利用迭代器来工作。我觉得那是一个两小时的枯燥演讲,但却非常受欢迎。不过我那时并没有想把这个东西塞在标准里,它毕竟是太过先进的编程技术,大概还不适于出现在现实世界里,恐怕那些做实际工作的人对它没什么兴趣。
我是在 9 月做这个演讲的,直到次年 (1994) 月,我都没往 ANSI 标准上动过什么脑筋。1 月 6 日,我收到Andy Koenig 的一封信 (他那时是标准文档项目编辑), 信中说如果我希望 STL 成为标准库的一部分,可以在 1 月 25 日之前提交一份建议到委员会。我的答复是:”Andy, 你发疯了吗?”, 他答复道:” 不错,是的我发疯了,为什么咱们不疯一次试试看?”
当时我们有很多代码,但是没有文档,更没有正式的建议书。李小姐和我每星期工作 80 小时,终于在期限之前写出一份正式的建议书。当是时也,只有 Andy 一个人知道可能会发生些什么。他是唯一的支持者,在那段日子里他确实提供了很多帮助。我们把建议寄出去了,然后就是等待。在写建议的过程中我们做了很多事。当你把一个东西写下来,特别是想到你写的可能会成为标准,你就会发现设计中的所有纰漏。寄出标准后,我们不得不一段一段重写了库中间的代码,以及几百个组件,一直到 3 月份圣迭戈会议之前。然后我们又重新修订了建议书,因为在重新写代码的过程中,我们又发现建议书中间的很多瑕疵。
Q: 您能描述一下当时委员会里的争论吗?建议一开始是被支持呢,还是反对?
A: 我当时无法预料会发生些什么。我做了一个报告,反响很好。但当时有许多反对意见。主要的意见是:这是一份庞大的建议,而且来得太晚,前一次会议上已经做出决议,不在接受任何大的建议。而这个东西是有史以来最大的建议,包括了一大堆新玩艺。投票的结果很有趣,压倒多数的意见认为应对建议进行再考虑,并把投票推迟到下次会议,就是后来众所周知的滑铁卢会议。
Bjarne Stroustrup 成了 STL 的强有力支持者。很多人都通过建议、更改和修订的方式给予了帮助。Bjarne 干脆跑到这来跟我们一起工作了一个礼拜。Andy 更是无时无刻的帮助我们。C++ 是一种复杂的语言,不是总能搞得清楚确切的含义的。差不多每天我都要问 Andy 和 Bjarne C++ 能不能干这干那。我得把特殊的荣誉归于 Andy, 是他提出把 STL 作为 C++ 标准库的一部分;而 Bjarne 也成了委员会中STL 的主要鼓吹者。其他要感谢的人还有:Mike Vilot,标准库小组的负责人; Rogue Wave 公司的Nathan Myers (Rogue Wave 是 Boland C++Builder 中 STL 方案的提供商 —— 译者),Andersen 咨询公司的 Larry Podmolik。确实有好多人要致谢。
在圣迭戈提出的 STL 实际与当时的 C++,我们被要求用新的 ANSI/ISO C++ 语言特性重写 STL,这些特性中有一些是尚未实现的。为了正确使用这些新的、未实现的 C++ 特性,Bjarne 和 Andy 花了无以计数的时间 来帮助我们。
人们希望容器独立于内存模式,这有点过分,因为语言本身并没有包括内存模式。所以我们得要想出一些机制来抽象内存模式。在 STL 的早期版本里,假定容器的容积可以用 size_t 类型来表示,迭代器之间的距离可以用 ptrdiff_t 来表示。现在我们被告知,你为什么不抽象的定义这些类型?这个要求比较高,连语言本身都没有抽象定义这些类型,而且 C/C++ 数组还不能被这些类型定义所限定。我们发明了一个机制称作 “allocator”,封装了内存模式的信息。这各机制深刻地影响了库中间的每一个组件。你可能疑惑:内存模式和算法或者容器类接口有什么关系?如果你使用 size_t 这样的东西,你就无法使用 T* 对象,因为存在不同的指针类型 (T*, T huge *, 等等)。这样你就不能使用引用,因为内存模式不同的话,会产成不同的引用类型。这样就会导致标准库产生庞大的分支。
另外一件重要的事情是我们原先的关联类型数据结构被扩展了。这比较容易一些,但是最为标准的东西总是很困难的,因为我们做的东西人们要使用很多年。从容器的观点看,STL 做了十分清楚的二分法设计。所有的容器类被分成两种:顺序的和关联的,就好像常规的内存和按内容寻址的内存一般。这些容器的语义十分清楚。
当我到滑铁卢以后,Bjarne 用了不少时间来安慰我不要太在意成败与否,因为虽然看上去似乎不会成功,但是我们毕竟做到了最好。我们试过了,所以应该坦然面对。成功的期望很低。我们估计大部分的意见将是反对。但是事实上,确实有一些反对意见,但不占上风。滑铁卢投票的结果让人大跌眼镜,80% 赞成,20% 反对。所有人都预期会有一场恶战,一场大论战。结果是确实有争论,但投票是压倒性的。
Q: STL 对于 1994 年 2 月发行的 ANSI/ISO C++ 工作文件中的类库有何影响?
A: STL 被放进了滑铁卢会议的工作文件里。STL 文档被分解成若干部分,放在了文件的不同部分中。MikeVilot 负责此事。我并没有过多地参与编辑工作,甚至也不是 C++ 委员会的成员。不过每次有关 STL 的建议都由我来考虑。委员会考虑还是满周到的。
Q: 委员会后来又做了一些有关模板机制的改动,哪些影响到了 STL?
A: 在 STL 被接受之前,有两个变化影响到了我们修订 STL。其一是类模板增加了包含函数模板的能力。STL广泛地使用了这个特性来允许你建立各种容纳容器的容器。一个单独的构造函数就能让你建立一个能容纳 list 或其他容器的的 vector。还有一个模板构造函数,从迭代器构造容器对象,你可以用一对迭代器当作参数传给它,这对迭代器之间的元素都会被用来构造新的容器类对象。另一个 STL 用到的新特性是把模板自身当作模板参数传给类模板。这项技术被用在刚刚提到的 allocator 中。
Q: 那么 STL 影响了模板机制吗?
A: 在弗基山谷的会议中,Bjarne 建议给模板增加一个 “局部特殊化”(partial specialization) 的特性。这个特性可以让很多算法和类效率更高,但也会带来代码体积上的问题。我跟 Bjarne 在这个建议上共同研究了一段时间,这个建议就是为了使 STL 更高效而提出的。我们来解释一下什么是 “局部特殊化”。你现在有一个函数模板 swap (T&, T&),用来交换两个参数。但是当 T 是某些特殊的类型参数时,你想做一些特殊的事情。例如对于 swap (int&, int&),你想用一种特别的操作来交换数据。这一点在没有局部特殊化机制的情况下是不可能的。有了局部特殊化机制,你可以声明一个函数模板如下:
template
这种形式给 vector 容器类的 swap 操作提供了一种特别的办法。从性能的角度讲,这是非常重要的。如果你用通用的形式去交换 vector,会使用三个赋值操作,vector 被复制三次,时间复杂度是线性的。然而,如果我们有一个局部特殊化的 swap 版本专门用来交换两个 vector,你可以得到一个时间复杂度为常数的,非常快的操作,只要移动 vector 头部的两个指针就 OK。这能让 vector 上的 sort 算法运行得更快。没有局部特殊化,让某一种特殊的 vector,例如 vector 运行得更快的唯一办法是让程序员自己定一个特殊的 swap 函数,这行得通,但是加重了程序员的负担。在大部分情况下,局部特殊化机制能够让算法在某些通用类上表现得更高效。你有最通用的 swap,不那么通用的 swap,更不通用的 swap,完全特殊的 swap这么一系列重载的 swap,然后你使用局部特殊化,编译器会自动找到最接近的那个 swap。另一个例子是copy。现在我们的 copy 就是通过迭代器一个一个地复制。使用模板特殊化可以定义一个函数模板:
template T** copy(T**, T**, T**);
这可以用 memcpy 高效地复制一系列指针来实现,因为是指针复制,我们可以不必担心构造对象和析构对象的开销。这个函数模板可以定义一次,然后供整个库使用,而且用户不必操心。我们使用局部特殊化处理了一些算法。这是个重要的改进,据我所知在弗基山谷会议上得到了好评,将来会成为标准的一部分。
Q: 除了标准类库外,STL 对那一类的应用程序来说最有用处?
A: 我希望 STL 能够引导大家学习一种新的编程风格:通用编程。我相信这种风格适用于任何种类的应用程序。这种风格就是:用最通用的方式来写算法和数据结构。这些结构所要求的语义特性应该能够被清楚地归类和分类,而这些归类分类的原则应该是任何对象都能满足的。理解和发展这种技术还要很长时间,STL 不过是这个过程的起点。
我们最终会对通用的组件有一个标准的分类,这些组件具有精心定义的接口和复杂度。程序员们将不必在微观层次上编程。你再也不用去写一个二分查找算法。就是在现在,STL 也已经提供了好几个通用的二分查找算法,凡是能用二分查找算法的场合,都可以使用这些算法。算法所要求的前提条件很少:你只要在代码里使用它。我希望所有的组件都能有这么一天。我们会有一个标准的分类,人们不用再重复这些工作。
这就是 Douglas McIlroy 的梦想,他在 1969 年关于 “构件工厂” 的那篇著名文章中所提出来的东西。STL就是这种 “构件工厂” 的一个范例。当然,还需要有主流的力量介入这种技术的发展之中,光靠研究机构不行,工业界应该想程序员提供组件和工具,帮助他们找到所需的组件,把组件粘合到一起,然后确定复杂度是否达到预期。
Q: STL 没有实现一个持久化 (persistent) 对象容器模型。map 和 multimap 似乎是比较好的候选者,它们可以 把对象按索引存入持久对象数据库。您在此方向上做了什么工作吗,或者对这类实现有何评论?
A:很多人都注意到这个问题。STL 没实现持久化是有理由的。STL 在当时已经是能被接受的最巨大的库了。再大一点的话,我认为委员会肯定不会接受。当然持久化是确实是一些人提出的问题。在设计 STL,特别是设计 allocator 时,Bjarne 认为这个封装了内存模式的组件可以用来封装持久性内存模式。Bjarne 的洞察秋毫非常的重要和有趣,好几个对象数据库公司正在盯着这项技术。1994 年 10 月我参加了 Object Database Management Group 的一个会议,我做了一个关于演说。他们非常感兴趣,想让他们正在形成中的组件库的接口与 STL 一致,但不包括 allocator 在内。不过该集团的某些成员仔细分析了 allocator 是否能够被用来实现持久化。我希望与 STL 接口一致的组件对象持久化方案能在接下来的一年里出现。
Q:set,multiset,map 和 multimap 是用红黑树实现的,您试过用其他的结构,比如 B* 树来实现吗?A:我不认为 B* 适用于内存中的数据结构,不过当然这件事还是应该去做的。应该对许多其他的数据结构,比如跳表 (skip list)、伸展树 (splay tree)、半平衡树 (half-balanced tree) 等,也实现 STL 容器的标准接口。应该做这样的研究工作,因为 STL 提供了一个很好的框架,可以用来比较这些结构的性能。结口是固定的,基本的复杂度是固定的,现在我们就可一个对各种数据结构进行很有意义的比较了。在数据结构领域里有很多人用各种各样的接口来实现不同的数据结构,我希望他们能用 STL 框架来把这些数据结构变成通用的。
Q:有没有编译器厂商跟您一起工作来把 STL 集成到他们的产品中去?
A:是的,我接到了很多厂家的电话。Borland 公司的 Peter Becker 出的力特别大。他帮助我实现了对应Borland 编译器的所有内存模式的 allocator 组件。Symantec 打算为他们的 Macintosh 编译器提供一个 STL实现。Edison 设计集团也很有帮助。我们从大多数编译器厂商都得到了帮助。
Q:STL 包括了对 MS-DOS 的 16 位内存模式编译器的支持,不过当前的重点显然是在 32 位上线性内存模式(flat model) 的操作系统和编译器上。您觉得这种面向内存模式的方案以后还会有效吗?
A:抛开 Intel 的体系结构不谈,内存模式是一个对象,封装了有关指针的信息:这个指针的整型尺寸和距离类型是什么,相关的引用类型是什么,等等。如果我们想利用各种内存,比如持久性内存,共享内存等等,抽象化的工作就非常重要了。STL 的一个很漂亮的特性是整个库中唯一与机器类型相关的部分 —— 代表真实指针,真实引用的组件 —— 被封装到大约 16 行代码里,其他的一切,容器、算法等等,都与机器无关(真是牛啊!)。从移植的观点看,所有及其相关的东西,象是地址记法,指针等等,都被封装到一个微小的,很好理解的机制里面。这样一来,allocator 对于 STL 而言就不是那么重要了,至少不像对于基本数据结构和算法的分解那么重要。
Q:ANSI/ISO C 标准委员会认为像内存模式这类问题是平台相关的,没有对此做出什么具体规定。C++ 委员会会不会采取不同的态度?为什么?
A:我认为 STL 在内存模式这一点上跟 C++ 标准相比是超前的。但是在 C 和 C++ 之间有着显著的不同。C++ 有构造函数和 new 操作符来对付内存模式问题,而且它们是语言的一部分。现在看来似乎让 new 操作符像 STL 容器使用 allocater 那样来工作是很有意义的。不过现在对问题的重要性不像 STL 出现之前那么显著了,因为在大多数场合,STL 数据结构将让 new 失业。大部分人不再需要分配一个数组,因为 STL 在做这类事情上更为高效。要知道我对效率的迷信是无以复加的,可我在我的代码里从不使用 new,汇编代码表明其效率比使用 new 时更高。随着 STL 的广泛使用,new 会逐渐淡出江湖。而且 STL 永远都会记住回收内存,因为当一个容器,比如 vector 退出作用域时,它的析构函数被调用,会把容器里的所有东西都析构。你也不必再担心内存泄漏了。STL 可以戏剧性地降低对于垃圾收集机制的需求。使用 STL 容器,你可以为所欲为,不用关心内存的管理,自有 STL 构造函数和析构函数来对付。
Q:C++ 标准库子委员会正在制订命名空间(namespace)和异常处理机制。STL 类会有名空间吗,会抛出异常吗?
A:是的。该委员会的几个成员正在考虑这件事,他们的工作非常卓越。
Q:现在的 STL 跟最终作为标准的 STL 会有多大不同?委员会会不会干预某些变化,新的设计会不会被严格地控制起来?
A:多数人的意见看起来是不希望对 STL 做任何重要的改变。
Q:在成为标准之前,程序员们怎样的一些 STL 经验?
A:他们可以从 butler.hpl.hp.com/stl 当下 STL 头文件,在 Borland 和 IBM 或其他足够强劲的的编译器中使用它。学习这种编程技术的唯一途径是编程,看看范例,试着用这种技术来编程。
Q:您正在和 P. J. Plauger 合作一本 STL 的书。那本书的重点是什么?什么时候面世?
A:计划 95 年夏天面世,重点是对 STL 实现技术的详解,跟他那本标准 C 库实现和标准 C++ 库实现的书类似。他是这本书的第一作者。该书可以作为 STL 的参考手册。我希望跟 Bjarne 合作另写一本书,在 C++/STL 背景下介绍语言与库的交互作用。
好多工作都等着要做。为了 STL 的成功,人们需要对这种编程技术进行更多的试验性研究,更多的文章和书籍应该对此提供帮助。要准备开设此类课程,写一些入门指南,开发一些工具帮助人们漫游 STL 库。STL 是一个框架,应该有好的工具来帮助使用这个框架。
Q:通用编程跟 OOP 之间有什么关系?
A:一句话,通用编程是 OOP 基本思想的自然延续。什么是 OOP 的基本思想呢?把组件的实现和接口分开,并且让组件具有多态性。不过,两者还是有根本的不同。OOP 强调在程序构造中语言要素的语法。你必须得继承,使用类,使用对象,对象传递消息。GP 不关心你继承或是不继承,它的开端是分析产品的分类,有些什么种类,他们的行为如何。就是说,两件东西相等意味着什么?怎样正确地定义相等操作?不单单是相等操作那么简单,你往深处分析就会发现 “相等” 这个一般观念意味着两个对象部分,或者至少基本部分是相等的,据此我们就可以有一个通用的相等操作。再说对象的种类。假设存在一个顺序序列和一组对于顺序序列的操作。那么这些操作的语义是什么?从复杂度权衡的角度看,我们应该向用户提供什么样的顺序序列?该种序列上存在那些操作?那种排序是我们需要的?只有对这些组件的概念型分类搞清楚了,我们才能提到实现的问题:使用模板、继承还是宏?使用什么语言和技术?GP 的基本观点是把抽象的软件组件和它们的行为用标准的分类学分类,出发点就是要建造真实的、高效的和不取决于语言的算法和数据结构。当然最终的载体还是语言,没有语言没法编程。STL 使用 C++,你也可以用 Ada 来实现,用其他的语言来实现也行,结果会有所不同,但基本的东西是一样的。到处都要用到二分查找和排序,而这就是人们正在做的。对于容器的语义,不同的语言会带来轻微的不同。但是基本的区别很清楚是 GP 所依存的语义,以及语义分解。例如,我们决定需要一个组件 swap,然后指出这个组件在不同的语言中如果工作。显然重点是语义以及语义分类。而 OOP 所强调的(我认为是过分强调的)是清楚的定义类之间的层次关系。OOP 告诉了你如何建立层次关系,却没有告诉你这些关系的实质。
Q:您对 STL 和 GP 的未来怎么看?
A:我刚才提到过,程序员们的梦想是拥有一个标准的组件仓库,其中的组件都具有良好的、易于理解的和标准的接口。为了达成这一点,GP 需要有一门专门的科学来作为基础和支柱。STL 在某种程度上开始了这项工作,它对于某些基本的组件进行了语义上的分类。我们要在这上面下更多的功夫,目标是要将软件工程从一种手工艺技术转化为工程学科。这需要一门对于基本概念的分类学,以及一些关于这些基本概念的定理,这些定理必须是容易理解和掌握的,每一个程序员即使不能很清楚的知道这些定理,也能正确地使用它。很多人根本不知道交换律,但只要上过学的人都知道 2+5 等于 5+2。我希望所有的程序员都能学习一些基本的语义属性和基本操作:赋值意味着什么?相等意味着什么?怎样建立数据结构,等等。
当前,C++ 是 GP 的最佳载体。我试过其他的语言,最后还是 C++ 最理想地达成了抽象和高效的统一。但是我觉得可能设计出一种语言,基于 C 和很多 C++ 的卓越思想,而又更适合于 GP。它没有 C++ 的一些缺陷,特别是不会像 C++ 一样庞大。STL 处理的东西是概念,什么是迭代器,不是类,不是类型,是概念。说得更正式一些,这是 Bourbaki 所说的结构类型(structure type),是逻辑学家所说的理念(theory),或是类型理论学派的人所说的种类(sort),这种东西在 C++ 里没有语言层面上的对应物(原文是 incarnation,直译为肉身 —— 译者),但是可以有。你可以拥有一种语言,使用它你可以探讨概念,精化概念,最终用一种非常 “程序化”(programmatic —— 译者)的手段把它们转化为类。当然确实有一些语言能处理种类(sorts),但是当你想排序(sort)时它们没什么用处。我们能够有一种语言,用它我们能定义叫做 foward iterator(前向迭代器)的东西,在 STL 里这是个概念,没有C++ 对应物。然后我们可以从 forword iterator 中发展出 bidirectional iterator(双向迭代器),再发展出 random iterator。可能设计一种语言大为简化 GP,我完全相信该语言足够高效,其机器模型与 C/C++ 充分接近。我完全相信能够设计出一种语言,一方面尽可能地靠近机器层面以达成绝对的高效,另一方面能够处理非常抽象化的实体。我认为该语言的抽象性能够超过 C++,同时又与底层的机器之间契合得天衣无缝。我认为 GP 会影响到语言的研究方向,我们会有适于 GP 的实用语言。从这些话中你应该能猜出我下一步的计划。
mengyan
译于2001年1月