Al Stevens 采访 Alex Stepanov

这篇采访刊登在 1995 年 3 月版的 Dr. Dobb's 杂志 上,经授权转载。


请您谈谈您对泛型编程的长期兴趣。

我从 70 年代后期就开始思考泛型编程,当时我注意到一些算法并不依赖于数据结构的特定实现,而只依赖于结构的一些基本语义属性。我开始研究许多不同的算法,发现大多数算法可以从特定实现中抽象出来,而不会损失效率。效率是我最关心的问题。以一种在实例化后会变得低效的方式对算法进行抽象是愚蠢的。

当时我认为进行此类研究的正确方法是开发一种编程语言,这就是我和我的两位朋友 Deepak Kapur 和 David Musser 开始做的事情。Deepak Kapur 目前是纽约州立大学奥尔巴尼分校的教授,David Musser 是伦斯勒理工学院的教授。当时我们三人都在纽约州斯肯尼克塔迪的通用电气研究中心工作。我们开始开发一种名为 Tecton 的语言,它可以让用户描述与我们称为泛型结构相关的算法,这仅仅是这些类型的形式类型和属性的集合。有点像数学的东西。我们意识到,可以在这些结构上定义操作代数,可以细化它们,可以丰富它们,还可以做很多事情。

有一些有趣的思想,但研究没有取得实际成果,因为 Tecton 是函数式的。我们相信 Backus 的观点,认为我们应该将编程从冯·诺依曼风格中解放出来,并且我们不想有副作用。这限制了我们处理许多需要状态和副作用概念的算法的能力。

关于 Tecton 的一件有趣的事情,我在 70 年代后期意识到,在抽象数据类型的公认概念中存在着一个根本的限制。人们通常将抽象数据类型视为只告诉您对象行为而实现完全隐藏的东西。人们普遍认为,操作的复杂性是实现的一部分,而抽象忽略了复杂性。我现在所理解的泛型编程的核心之一是,复杂性,或者至少是一些关于复杂性的通用概念,必须与操作相关联。

让我们举个例子。考虑一个抽象数据类型堆栈。仅仅拥有 Push 和 Pop 与公理连接起来是不够的,在这个公理中,您将某个东西压入堆栈,在您弹出堆栈后,您会得到相同的东西。最重要的是,压入堆栈是一个恒定时间操作,与堆栈的大小无关。如果我实现堆栈的方式是,每次压入堆栈都会变得越来越慢,那么没有人会想要使用这个堆栈。

我们需要将实现与接口分离,但不能以完全忽略复杂性的代价为之。复杂性必须并且是模块与其用户之间不成文契约的一部分。引入抽象数据类型概念的原因是为了允许可互换的软件模块。除非这些模块共享类似的复杂性行为,否则您无法拥有可互换的模块。如果我用另一个功能行为相同但复杂性权衡不同的模块替换一个模块,那么使用这段代码的用户将会感到不愉快。我可以告诉他任何我喜欢的关于数据抽象的东西,但他仍然不愿意使用这段代码。复杂性断言必须是接口的一部分。

大约在 1983 年,我从通用电气研究中心搬到了纽约理工大学(以前称为布鲁克林理工大学)的教职岗位。我开始研究图算法。我的主要合作者是 Aaron Kershenbaum,他现在在 IBM 约克镇高地工作。他是图和网络算法方面的专家,我让他相信,高阶和泛型编程的一些思想适用于图算法。他有一些拨款,并为我提供支持,让我开始和他合作,将这些思想应用于真实的网络算法。他对构建一个高阶泛型组件工具箱感兴趣,以便可以实现其中的一些算法,因为一些网络算法非常复杂,虽然它们在理论上得到了分析,但从未实现过。我决定使用一种名为 Scheme 的 Lisp 方言来构建这样的工具箱。Aaron 和我在 Scheme 中开发了一个大型组件库,展示了各种编程技术。网络算法是主要目标。后来,仍在通用电气研究中心的 Dave Musser 加入我们,我们开发了更多组件,一个相当大的库。这个库在大学被研究生使用,但从未在商业上使用过。在这一过程中,我意识到副作用很重要,因为您无法在没有副作用的情况下进行图操作。您不能在每次想要修改顶点时都复制一个图。因此,当时的见解是,在构建泛型算法时,可以将高阶技术与纪律性的副作用使用相结合。副作用不一定不好;它们只有在被滥用时才不好。

在 1985 年夏天,我被邀请回到通用电气研究中心,为高阶编程开设课程。我演示了如何使用这种技术构建复杂的算法。参加课程的人中有一个叫 Art Chen 的人,当时是信息系统实验室的经理。他对此印象深刻,并问我是否可以使用这些技术在 Ada 中构建一个工业级强度库,前提是我会得到支持。作为一个贫穷的助理教授,我答应了,尽管当时我还不了解 Ada。我和 Dave Musser 合作构建了这个 Ada 库。这是一项重要的工作,因为从像 Scheme 这样的动态类型语言切换到像 Ada 这样的强类型语言,让我意识到强类型的意义。每个人都意识到强类型有助于捕获错误。我发现,在 Ada 泛型的情况下,强类型也是捕获设计的工具。它不仅仅是捕获错误的工具。它也是思考的工具。这项工作导致了组件空间正交分解的思想。我意识到软件组件属于不同的类别。面向对象编程爱好者认为一切都是对象。当我开发 Ada 泛型库时,我意识到并非如此。有些东西是对象。拥有状态并改变其状态的东西是对象。还有一些东西不是对象。二分搜索不是对象。它是一种算法。此外,我意识到,通过将组件空间分解成几个正交维度,我们可以减少组件的数量,更重要的是,我们可以提供一个关于如何设计事物的概念框架。

然后我被贝尔实验室聘用,在 C++ 团队工作,负责 C++ 库。他们问我是否可以在 C++ 中做到这一点。当然,我当时还不了解 C++,当然,我说我可以。但我不能在 C++ 中做到这一点,因为在 1987 年,C++ 还没有模板,而模板对于启用这种编程风格至关重要。继承是获得泛型的唯一机制,它还不够。

即使现在,C++ 继承对于泛型编程也没有多大用处。让我们讨论一下原因。许多人试图使用继承来实现数据结构和容器类。正如我们现在所知,几乎没有成功的尝试。C++ 继承及其相关的编程风格受到极大的限制。使用它来实现像相等这样简单的事情的设计是不可能的。如果您从您的层次结构根部的基类 X 开始,并在此类上定义一个接收类型为 X 的参数的虚拟相等运算符,然后从类 X 派生类 Y。相等的接口是什么?它具有比较 Y 和 X 的相等性。以动物为例(OO 人喜欢动物),定义哺乳动物并从哺乳动物派生长颈鹿。然后定义一个成员函数 mate,其中动物与动物交配并返回一个动物。然后您从动物派生长颈鹿,当然,它有一个函数 mate,其中长颈鹿与动物交配并返回一个动物。这绝对不是您想要的。虽然交配对于 C++ 程序员来说可能不那么重要,但相等性是重要的。我不知道有任何算法不使用某种形式的相等性。

您需要模板来处理此类问题。您可以拥有一个模板类动物,它具有成员函数 mate,它接受动物并返回动物。当您实例化长颈鹿时,mate 将执行正确的事情。从这方面来说,模板是一个更强大的机制。

但是,我能够构建一个相当大的算法库,后来成为 Unix 系统实验室标准组件库的一部分。我在贝尔实验室通过与 Andy Koenig 和 Bjarne Stroustrup 等人谈论编程,学到了很多东西。我意识到 C/C++ 是一种重要的编程语言,它具有一些不可忽视的基本范式。我特别了解指针非常有用。我不是指悬空指针。我不是指指向堆栈的指针。但我的意思是指针的通用概念是一个强大的工具。地址的概念被普遍使用。人们错误地认为指针使我们的思维变得顺序化。并非如此。如果没有某种地址,我们就无法描述任何并行算法。如果您试图描述 n 个数字的并行加法,除非您可以谈论第一个数字加到第二个数字,而第三个数字加到第四个数字,否则您无法做到。您需要某种索引。您需要某种地址来描述任何类型的算法,无论是顺序的还是并行的。地址或位置的概念在我们对计算过程(算法)的理解中是至关重要的。

现在让我们考虑一下为什么 C 是一门很棒的语言。人们普遍认为 C 是一种黑客语言,它之所以成功是因为 Unix 是用它编写的。我不同意。在很长一段时间内,计算机体系结构的演变,不是因为一些聪明的人想出了如何演变体系结构——事实上,在那个时期,聪明的人一直在推动带标签的体系结构——而是因为不同的程序员为了解决实际问题而提出的需求。能够处理数字的计算机发展成为具有字节寻址内存、扁平地址空间和指针的计算机。这是一种自然演变,反映了人们正在解决的越来越多的问题。C 反映了 Dennis Ritchie 的天才,它提供了已经发展了 30 年的计算机的最小模型。C 不是一个快速的破解。随着计算机不断发展以处理各种问题,C 作为这种计算机的最小模型,成为了一种非常强大的语言,可以非常有效地解决不同领域各种问题。这就是 C 可移植性的秘密:它是我们所拥有的抽象计算机的最佳表示。当然,抽象是在一组真实计算机上完成的,而不是一些想象的计算设备。此外,人们可以理解 C 背后的机器模型。对于一个普通的工程师来说,理解 C 背后的机器模型比理解 Ada 甚至 Scheme 背后的机器模型要容易得多。C 成功是因为它做对了事情,而不是因为 AT&T 促进了它,或者 Unix 是用它编写的。

C++ 的成功在于,Bjarne 并没有试图凭空创造机器模型,而是以 C 为基础,试图让 C 进一步发展,允许更通用的编程技术,但仍然保持这个机器模型的框架。C 的机器模型非常简单。你有内存,事物就存在于内存中。你有指向内存中连续元素的指针。这很容易理解。C++ 保留了这个模型,但使内存中的事物比 C 的机器模型中更为扩展,因为 C 有一组有限的数据类型。它有结构,允许一种可扩展的类型系统,但它不允许你在结构上定义操作。这限制了类型系统的可扩展性。C++ 将 C 的机器模型更进一步地推动,朝着真正可扩展的类型系统发展。

1988 年,我加入了惠普实验室,受聘于泛型库的开发工作。几年来,我一直没有进行泛型库的开发,而是从事磁盘驱动器的开发工作,虽然激动人心,但却与这个研究领域完全无关。1992 年,我回到了泛型库的开发工作,当时我的实验室主任 Bill Worley 与我共同组建了一个算法项目,我担任该项目的负责人。C++ 当时已经有了模板。我发现 Bjarne 在模板的设计方面做得非常出色。早些时候,我在贝尔实验室参加了几次关于模板设计的讨论,我曾与 Bjarne 激烈争论,认为他应该让 C++ 模板尽可能接近 Ada 的泛型。我想我当时争论得太过激烈,以至于他最终没有采纳我的意见。我意识到在 C++ 中拥有模板函数的重要性,而不仅仅是模板类,正如一些人所相信的那样。然而,我认为模板函数应该像 Ada 泛型一样工作,即它们应该被显式地实例化。Bjarne 没有听我的建议,他设计了一种模板函数机制,其中模板是使用重载机制隐式地实例化的。这种特定技术对我的工作至关重要,因为我发现它可以让我做很多在 Ada 中无法做到的事情。我认为 Bjarne 的这种特殊设计是一项了不起的工作,我很高兴他没有听我的建议。

你是什么时候开始构思 STL 的,它的最初目的是什么?

1992 年,项目成立时,共有 8 个人参与。随着时间的推移,团队逐渐缩减,最终只剩下我和孟丽两个人。虽然孟丽对这个领域并不熟悉——她大部分职业生涯都在从事编译器工作——但她接受了泛型编程研究的总体愿景,并相信它可以改变软件开发,而当时只有很少人认同这个观点。我认为没有她的帮助,我将无法构建 STL。(毕竟,STL 代表着 Stepanov 和 Lee...)我们编写了一个巨大的库,包含大量代码、数据结构、算法、函数对象、适配器等等。代码很多,但没有文档。我们的工作被视为一个研究项目,其目标是证明可以将算法尽可能泛化地定义,并且仍然保持极高的效率。我们花了很多时间进行测量,发现我们可以使这些算法尽可能通用,并且仍然像手工编写的代码一样高效。这种编程风格没有性能损失!库在不断发展,但作为项目,它的方向并不明确。几个幸运事件将它引向了 STL。

你是什么时候决定将 STL 作为 ANSI/ISO 标准 C++ 定义的一部分,以及为什么?

1993 年夏天,Andy Koenig 来斯坦福大学教授 C++ 课程。我向他展示了我们的一些东西,我认为他对此非常兴奋。他安排我参加了 11 月在圣何塞举行的 ANSI/ISO C++ 标准委员会会议,并发表演讲。我发表了一个名为 "C++ 编程的科学" 的演讲。演讲内容比较理论化。主要观点是,在 C++ 元素的基本操作之间存在着一些基本规律,必须遵守这些规律。我展示了一组连接构造函数、赋值和相等运算符等非常基本的操作的规律。C++ 语言本身没有强制任何约束。你可以定义你的相等运算符来执行乘法。但相等就是相等,它应该是一个自反运算。A 应该等于 A。它应该对称。如果 A 等于 B,那么 B 应该等于 A。它还应该是传递的。标准数学公理。相等对于其他运算至关重要。有一些公理连接构造函数和相等。如果你使用复制构造函数用另一个对象构造一个对象,那么这两个对象应该是相等的。C++ 没有强制规定这一点,但这是我们必须遵守的基本规律之一。赋值必须创建相等的物件。因此,我提出了一系列公理,连接了这些基本运算。我谈了谈迭代器的公理,并展示了一些在迭代器上运行的通用算法。这是一场时长两小时的演讲,我认为它相当枯燥。然而,它受到了很好的反响。当时,我没有想过将它作为标准的一部分,因为人们普遍认为,这是一种高级编程技术,在 "现实世界" 中不会被使用。我认为实用主义者对这项工作毫无兴趣。

我是在 11 月份发表的演讲,直到 1 月份才开始考虑 ANSI。1 月 6 日,我收到了 Andy Koenig 的一封邮件,他是标准文档的项目编辑,他说如果我想让我的库成为标准的一部分,我应该在 1 月 25 日之前提交提案。我的回答是,"Andy,你疯了吗?",他回答说,"好吧,我是疯了,但为什么不试一试呢?"

当时,代码很多,但没有文档,更不用说正式的提案了。我和孟丽每周工作 80 小时,以便在邮件截止日期之前提交提案。在此期间,唯一知道这件事的人是 Andy。他是唯一的支持者,并且在这个时期给予了我们很多帮助。我们在提交提案时定义了很多东西。当你把事情写下来,尤其是当你将它们作为标准提出时,你会发现你设计中的各种缺陷。我们必须重新实现库中的每一行代码,包括数百个组件,时间跨度从 1 月份的邮件提交到 3 月份在圣地亚哥的下一场会议。然后我们不得不修改提案,因为在编写代码的过程中,我们发现了许多缺陷。

你能描述一下委员会在提案之后进行的讨论和辩论吗?当时是否有立即的支持?反对吗?

我们不相信会有任何结果。我发表了一篇演讲,受到了很好的反响。有很多反对意见,大多数意见是这样的:这是一个巨大的提案,时间已经太晚了,在上一场会议上已经通过了一项决议,不接受任何重大的提案,而现在又出现了这个庞然大物,是迄今为止最大的提案,里面包含了许多全新的东西。投票开始了,有趣的是,绝大多数人投票赞成在下一场会议上审查该提案,并在下一场会议上(在安大略省的滑铁卢举行)进行投票。

Bjarne Stroustrup 成为 STL 的坚定支持者。许多人提出了建议、修改和修订。Bjarne 来这里工作了一周。Andy 始终给予帮助。C++ 是一门复杂的语言,因此,一个特定的结构的含义并不总是明确的。我几乎每天都打电话给 Andy 或 Bjarne,询问某某东西是否可以在 C++ 中实现。我应该特别感谢 Andy。他将 STL 构思为标准库的一部分。Bjarne 成为 STL 在委员会中的主要推动者。还有其他一些人也提供了帮助:图书馆组的负责人 Mike Vilot,Rogue Wave 的 Nathan Myers,安达信咨询公司的 Larry Podmolik。还有许多其他人。

我们在圣地亚哥提出的 STL 是用现在的 C++ 编写的。我们被要求使用新的 ANSI/ISO 语言特性重新编写它,其中一些特性尚未实现。Bjarne 和 Andy 花了大量时间试图验证我们是否正确地使用了这些尚未实现的特性,这让他们俩都非常忙碌。

人们希望容器独立于内存模型,这有点过分,因为语言中没有包含内存模型。人们希望库提供一些机制来抽象内存模型。早期版本的 STL 假设容器的大小可以用size_t类型的整数来表示,两个迭代器之间的距离可以用ptrdiff_t类型来表示。现在有人问我们,为什么不从这些方面抽象出来呢?这是一个很高的要求,因为语言本身并没有从这些方面抽象出来;C 和 C++ 数组没有被这些类型参数化。我们发明了一种叫做 "分配器" 的机制,它封装了有关内存模型的信息。这给库中的每个组件都带来了严重的后果。你可能想知道内存模型与算法或容器接口有什么关系。如果你不能使用诸如size_t之类的東西,你也不能使用諸如T*之类的東西,因为指针类型不同(T*, T huge *等等)。然后你不能使用引用,因为不同的内存模型有不同的引用类型。这对库的影响是巨大的。

第二件大事是扩展我们最初的数据结构,加入关联数据结构。这比较容易,但制定一个标准总是很困难,因为我们需要一些人们在未来几年中可以用来创建容器的东西。从容器的角度来看,STL 具有非常清晰的二分法。它提供了两种基本的容器类:序列容器和关联容器。它们就像普通内存和内容寻址内存。它有一个清晰的语义,解释了这些容器的功能。

当我到达滑铁卢时,Bjarne 花了很多时间向我解释,我不应该担心,很有可能它会失败,但我们尽了最大努力,我们尝试了,我们应该勇敢。人们的期望很低。我们预计会遇到重大反对意见。的确有一些反对意见,但并不严重。在滑铁卢进行投票时,结果完全出乎意料,因为支持率可能在 80%,反对率在 20%。每个人都预料到一场战斗,每个人都预料到争议。战斗是存在的,但投票结果是压倒性的。

STL 对 ANSI/ISO 1994 年 2 月工作文件发布的类库有什么影响?

STL 在滑铁卢被纳入了工作文件。STL 文档被拆分,并被放入工作文件库部分的不同位置。Mile Vilot 负责这项工作。我不积极参与编辑工作。我不是委员会成员,但每当有人提出与 STL 相关的更改时,都会先由我进行审核。委员会非常考虑我的意见。

委员会已经接受了几个模板更改。哪些更改对 STL 产生了影响?

在 STL 被接受之前,修改后的 STL 使用了两个变化。一个是拥有模板成员函数的能力。STL 广泛地使用它们来允许您从任何其他类型的容器构造任何类型的容器。有一个单一的构造函数允许您从列表或其他容器构造向量。有一个模板化的构造函数,它是在迭代器上进行模板化的,因此如果您向容器构造函数提供一对迭代器,则该容器将根据这些迭代器指定的范围内的元素进行构造。范围是由一对迭代器、泛化指针或地址指定的元素集合。

STL 的要求是否影响了任何提出的模板更改?

在 Valley Forge,Bjarne 提出了一个对模板的重要补充,称为“部分特化”,它将使许多算法和类更有效,并且将解决代码大小问题。我和 Bjarne 一起研究了这个提议,它是由使 STL 更加高效的需求驱动的。让我解释一下部分特化是什么。目前,您可以有一个由类 T 参数化的模板函数,称为swap(T&, T&),并交换它们。这是最通用的swap。如果您想要专门化swap并对特定类型执行不同的操作,您可以使用一个函数swap(int&, int&),它以不同的方式执行整数交换。但是,不可能进行中间部分特化,即提供以下形式的模板函数

  template <class T> void swap(vector<T>&, vector<T>&);
此形式提供了一种特殊的交换向量的方法。从效率的角度来看,这是一个重要的问题。如果您使用最通用的swap交换向量,它使用三个赋值,向量将被复制三次,这需要线性时间。但是,如果我们对swap进行部分特化,以交换两个向量,那么您可以获得快速、常数时间的操作,它在向量头中移动几个指针。这将允许sort例如,在向量向量上更快地工作。使用当前的 STL,没有部分特化,使之更快工作的唯一方法是为任何特定类型的vector,例如vector<int>,定义自己的swap,这可以完成,但这给程序员带来了负担。在许多情况下,部分特化将使算法在某些通用类上更有效。您可以拥有最通用的swap,一个不太通用的swap,一个更不通用的swap,以及一个完全特定的swap。您可以进行部分特化,编译器将找到最接近的匹配。另一个例子是copy。目前,copy算法只是遍历迭代器定义的元素序列并逐个复制它们。但是,使用部分特化,我们可以定义一个模板函数
  template <class T> T ** copy(T**,T**,T**);
这将使用memcpy高效地复制指针范围,因为当我们复制指针时,我们不必担心构造和析构,并且可以使用memcpy直接移动位。这可以在库中一次性完成,用户无需担心。我们可以为某些类型提供算法的特定特化。这是一个非常重要的变化,据我所知,它在 Valley Forge 收到了好评,并将成为标准的一部分。

除了标准类库之外,STL 最适合哪种应用程序?

我希望 STL 将引入一种称为泛型编程的编程风格。我相信这种风格适用于任何类型的应用程序,即尝试以最通用的方式编写算法和数据结构。指定满足共同语义要求的此类结构的族或类别是一种普遍的范式,适用于任何事物。在技术被理解和发展之前,将需要很长时间。STL 是这种类型的编程的起点。

最终,我们将拥有具有明确定义的接口和明确定义的复杂性的通用组件标准目录。程序员将不再在微观层面上进行编程。您将不再需要编写二分查找例程。即使现在,STL 也提供了以最通用的方式编写的几种二分查找算法。任何可进行二分搜索的内容都可以通过这些算法进行搜索。算法假定的最低要求是代码使用的唯一要求。我希望所有软件组件都会发生同样的事情。我们将拥有标准目录,人们将不再编写这些东西。

这是 Doug McIlroy 在 1969 年发表的一篇著名论文中谈论组件工厂时的梦想。STL 是能够实现此类组件工厂的编程技术的示例。当然,需要付出重大努力,不仅是研究工作,还有工业努力,为程序员提供此类目录,拥有允许人们找到他们需要的组件的工具,将组件粘合在一起,并验证他们的复杂性假设是否得到满足。

STL 没有实现持久对象容器模型。该mapmultimap容器特别适合作为持久对象数据库的倒排索引的持久存储容器。您是否在这方面做过任何工作,或者您可以评论此类实现吗?

这一点已被许多人注意到。STL 没有实现持久性,原因很简单。STL 的规模在当时是可想象的。我认为任何更大的组件集都不会通过标准委员会。但持久性是许多人考虑过的事情。在设计 STL 期间,尤其是在设计分配器组件期间,Bjarne 观察到,封装内存模型的分配器可以用于封装持久内存模型。这个见解是 Bjarne 的,它是一个重要且有趣的见解。几家对象数据库公司正在研究这一点。1994 年 10 月,我参加了对象数据库管理组的会议。我发表了关于 STL 的演讲,那里有强烈的兴趣使他们新兴接口中的容器符合 STL 标准。他们并没有关注分配器本身。但是,该组的某些成员正在调查是否可以使用分配器来实现持久性。我预计在明年内将会有具有符合 STL 标准的接口的持久对象存储,这些接口适合 STL 框架。

Set,multiset, mapmultimap使用红黑树数据结构实现。您是否尝试过其他结构,例如 B*树?

我认为这对于内存中的数据结构来说并不完全正确,但这需要完成。STL 定义的相同接口需要使用其他数据结构来实现——跳跃列表、伸展树、半平衡树等等。这是一个需要进行的主要研究活动,因为 STL 为我们提供了一个框架,我们可以比较这些结构的性能。接口是固定的。基本复杂度要求是固定的。现在我们可以进行有意义的实验,将不同的数据结构进行比较。来自数据结构社区的许多人提出了各种数据结构来满足这种接口。我希望他们会将它们作为通用数据结构在 STL 框架内实现。

编译器供应商是否正在与您合作将 STL 实现到他们的产品中?

是的。我接到了很多来自编译器供应商的电话。Borland 的 Pete Becker 提供了极大的帮助。他通过编写一些代码来帮助我们为所有 Borland 编译器的内存模型实现分配器。Symantec 将为他们的 Macintosh 编译器发布一个 STL 实现。Edison Design Group 一直非常有帮助。我们得到了大多数编译器供应商的大力支持。

STL 包含支持 16 位 MS-DOS 编译器内存模型的模板。随着当前对 32 位平面模型编译器和操作系统的重视,您认为内存模型方向是否仍然有效?

无论 Intel 架构如何,内存模型都是一个对象,它封装了有关什么是指针、与该指针相关的整数大小和差异类型是什么、与该指针相关的引用类型是什么等等的信息。如果我们引入其他类型的内存,例如持久内存、共享内存等等,抽象这一点很重要。STL 的一个很好的特性是,在 STL 中提到与机器相关的类型的唯一位置——指的是真实指针、真实引用——被封装在大约 16 行代码中。其他所有内容,所有容器、所有算法,都是抽象地构建的,没有提到任何与机器相关的方面。从可移植性的角度来看,所有与地址、指针概念相关的机器特定内容都被封装在一个微小且易于理解的机制中。但是,分配器对于 STL 来说不是必需的,不像基本数据结构和算法的分解那么重要。

ANSI/ISO C 标准委员会将平台特定的问题(如内存模型)视为实现细节,并未尝试对其进行编码。C++ 委员会将对这些问题采取不同的看法吗?如果是,为什么?

我认为从内存模型的角度来看,STL 超越了 C++ 标准。但是,C 和 C++ 之间存在显着差异。C++ 具有构造函数和 operator new,它们处理内存模型,并且是语言的一部分。现在可能需要考虑将诸如 operator new 之类的东西泛化,以便能够像 STL 容器接受分配器一样接受分配器。现在不像 STL 被接受之前那么重要,因为 STL 数据结构将消除使用 new 的大多数需求。大多数人不应该分配数组,因为 STL 在此方面做得很好。我从来不需要在我的代码中使用 new,而且我非常关注效率。代码往往比使用 new 更有效。随着 STL 的接受,new 将逐渐消失。STL 还解决了删除问题,例如,在向量的情况下,析构函数将在退出块时销毁它。您无需担心释放存储,就像使用 new 时一样。STL 可以显着减少对垃圾回收的需求。对容器的有纪律的使用允许您在没有自动内存管理的情况下完成所有需要做的事情。STL 构造函数和析构函数可以正确地分配内存。

C++ 标准库子委员会正在为异常处理定义标准命名空间和约定。STL 类是否将具有命名空间并抛出异常?

是的,他们会。委员会成员正在处理此事,他们做得很好。

最终的标准定义与当前的 STL 定义有什么不同?委员会会影响更改,还是设计受到更严格的控制?

似乎达成了一种共识,即不应该对 STL 进行任何重大更改。

程序员如何在 STL 成为标准之前尽早体验它?

他们可以从butler.hpl.hp.com下载 STL 头文件/stl并用 Borland 或 IBM 编译器,或任何其他功能强大的编译器来使用它,以处理 STL。学习某种编程风格的唯一方法是编程。他们需要查看示例并用这种风格编写程序。

您正在与 P.J. (Bill) Plauger 合作编写一本关于 STL 的书籍。这本书的重点是什么,预计何时出版?

这本书预计在 1995 年夏季出版,将是一本带注释的 STL 实现。它将类似于比尔关于标准 C 库和 C++ 标准草案库的书籍。他将负责这本书,它将作为使用 STL 的标准参考文档。我希望与 Bjarne 合作撰写一篇论文,探讨 C++/STL 上下文中的语言/库交互。它可能会导致另一本书的出版。

还需要做很多工作。为了让 STL 取得成功,人们应该做一些研究,尝试使用这种编程风格。需要编写更多书籍和文章来解释如何用这种风格编程。需要开发课程。需要编写教程。需要构建工具来帮助人们浏览库。STL 是一个框架,如果有一个工具可以帮助我们浏览这个框架,那就太好了。

泛型编程与面向对象编程之间的关系是什么?

从某种意义上说,泛型编程是面向对象编程基本思想的自然延续——将组件的接口和实现以及多态行为分开。但是,两者之间存在着根本差异。面向对象编程强调程序构建中语言元素的语法。您必须使用继承,必须使用类,必须使用对象,对象发送消息。泛型编程并不从是否使用继承开始。它从尝试对事物进行分类或产生分类开始,即存在哪些类型的事物,以及它们如何行为。也就是说,两件事相等意味着什么?定义等式的正确方法是什么?不仅仅是等式操作。您可以更深入地分析等式,并发现存在一个通用的等式概念,其中两个对象相等,如果它们的部分,或至少它们的基本部分相等。我们可以有一个通用的等式操作配方。我们可以讨论存在哪些类型的对象。有序列。有对序列的操作。这些操作的语义是什么?从复杂度权衡的角度来看,我们应该为用户提供哪些类型的序列?序列中存在哪些类型的算法?我们需要哪些类型的排序函数?只有在开发完这些之后,在我们拥有组件的概念分类之后,我们才会解决如何实现它们的问题。我们使用模板吗?我们使用继承吗?我们使用宏吗?我们使用什么类型的语言技术?泛型编程的基本思想是将抽象软件组件及其行为进行分类,并提出一个标准分类。起点是真实有效的算法和数据结构,而不是语言。当然,它总是体现在语言中。您无法在语言之外进行泛型编程。STL 是用 C++ 完成的。您可以在 Ada 中实现它。您可以在其他语言中实现它。它们会有所不同,但有一些基本的东西会存在。二分搜索必须无处不在。排序必须无处不在。这就是人们所做的事情。容器的语义会有一些修改,由语言强加的轻微修改。在一些语言中,您可以更多地使用继承,在一些语言中,您必须使用模板。但根本区别在于,泛型编程从语义和语义分解开始。例如,我们决定需要一个名为swap的组件。然后,我们找出这个特定组件如何在不同的语言中工作。重点在于语义和语义分类,而面向对象,特别是随着它的发展,更强调,我认为过分强调了如何开发事物,即使用类层次结构。OOP 告诉您如何构建类层次结构,但它没有告诉您这些类层次结构中应该包含什么。

您如何看待 STL 和泛型编程的未来?

我之前提到过一个梦想,即程序员拥有抽象组件的标准存储库,这些组件具有接口,这些接口被很好地理解并且符合常见的范式。要做到这一点,需要付出更多努力来开发这种编程风格的科学基础。STL 在一定程度上通过对一些基本组件的语义进行分类来启动它。我们需要在这方面做更多工作。目标是将软件工程从一门手艺转变为一门工程学科。它需要一个基本概念的分类和一些支配这些概念的定律,这些定律被很好地理解,可以被教授,每个程序员都知道,即使他不能正确地陈述它们。许多人知道算术,即使他们从未听说过交换律。每个高中毕业生都知道 2+5 等于 5+2。但并非所有的人都了解它是加法的交换律。我希望大多数程序员都能学会基本操作的基本语义属性。赋值是什么意思?平等是什么意思?如何构建数据结构。

目前,C++ 是这种编程风格的最佳载体。我已经尝试过不同的语言,我认为 C++ 允许抽象性和效率的奇妙结合。但是,我认为可以设计一种基于 C 和 C++ 带给世界的许多见解的语言,这种语言更适合这种编程风格,它缺少 C++ 的一些缺陷,特别是它庞大的体积。STL 处理称为概念的事物。什么是迭代器?不是一个类。不是一个类型。它是一个概念。(或者,如果我们想更正式一点,它是布尔巴基所说的结构类型,逻辑学家所说的理论,或者类型论者所说的排序。)它是 C++ 中没有语言化身的东西。但它可以。您可以拥有一种语言,您可以在其中讨论概念,细化它们,然后以非常编程的方式将它们最终形成类。(当然,有一些语言处理排序,但如果您想排序,它们并没有什么用。)我们可以拥有一种语言,我们可以在其中定义一个称为前向迭代器的东西,它仅仅在 STL 中被定义为一个概念——它没有 C++ 化身。然后,我们可以将前向迭代器细化为双向迭代器。然后,随机迭代器可以从中细化出来。有可能设计一种语言,它将为这种编程风格提供更大的便利性。我完全相信它必须像 C 和 C++ 一样高效,并且靠近机器。我相信可以构建一种语言,一方面可以紧密地逼近机器,另一方面具有处理非常抽象实体的能力。我认为抽象性可以比 C++ 中更大,而不会在底层机器之间产生差距。我认为泛型编程可以影响语言研究,并且我们将拥有实用的语言,这些语言易于使用,并且非常适合这种编程风格。从这一点,你可以推断出我接下来要做什么。

版权所有 © 1995 Dr. Dobb's Journal

[Silicon Surf] [STL Home]
版权所有 © 1996 Silicon Graphics, Inc. 保留所有权利。 商标信息