荣耀: 和大多数人一样,我认为C++ 缺乏一个大一统的库是阻碍C++ 更为广泛地使用的关键原因,您认为现在C++ 社群有足够的资源来开发一个像Java 或.NET 那般规模的库了吗?如果没有,我们该怎么做?我发现使用形形色色的第三方库非常不方便(一个插曲。我在使用微软Visual C++ 时,有时希望使用STL 组件,例如vector ,但由于我大幅使用了MFC ,而MFC 中也有类似的容器,所以,虽然vector 更好用,但为了避免因链接两个不同的库而导致文件体积增大,我最终往往放弃使用标准库。不过,倘若标准库提供了MFC 所提供的所有功能,我将肯定全部改用标准库)。
Bjarne: 毫无疑问,MFC 是迄今为止被广泛运用的最糟糕的基础库。它违反了一个好的C++ 设计应该遵循的大多数原则。它严重地扭曲了许多程序员对于“什么是C++ ”的看法!
当然,我也认为缺乏全面且标准的基础库是C++ 社群的一个主要问题。对于个体程序员来说,这也许是他们面对的最困难的问题。我在《C++ 语言的设计与演化》中谈到了这一点,至今我仍然坚持这一点。
C++ 社群没有一个有钱的公司来支持“平台中立的”标准库的开发 — 从来没有,跟其它专有语言(以及它们的“标准”库)相比,这一直是阻碍C++ 发展的一道藩篱。和专有的基础库的扩展速度相比,我们对标准库的扩展是很慢的(不过和其他ISO 标准库相比,我们的扩展速度应该算是比较快的)。我们还期望能够和新兴的非标准库(译注:如B oost ) 逐渐达成更为平滑的整合。别忘了,当年MFC 以及其它“后80 年代”风格的库被设计出来投放市场之际,尚无任何标准库可作它们的构建基础。因此,我希望今后的专有库和开源库都能充分尊重标准库,以便使它们之间的互操作变得容易 — 至少容易一点点。
C++ 社群并不是为大规模设计和实现而组织的。C++ 社群中也没有什么传统或惯例。和其它社群相比,我们缺少一个“统帅”,他可以有效地激励新库的创建,否决或“保佑”我们的努力和成果。然而,我对“统帅模式”是否可行心里没底 — 除非你所做的事情只是基本的模仿而已。Boost (www.boost.org )是一个优秀的成果,它的某些方面已经超出了模仿的范畴,然而它仍然缺乏一个明确的目标以及用于维持自身发展的权威机构或权威人物。
在以下三个相关的领域中,大规模的合作努力对于C++ 社群而言是必需且有意义的:基本的并发编程库(包括线程、锁和lock-free 算法等);平台无关的操作系统服务库(目录和文件操纵以及套接字等);以及GUI 编程库。前两个看起来是可行的,但要想建立一个标准的GUI 库,也许从技术上、经济上尤其是政治上都显得太困难了。
荣耀: 是的,我相信对于许多C++ 程序员而言,一个标准的GUI 库是极其重要的。许多人以为GUI 库不是一个太复杂的问题,但我却认为这可能是最棘手的问题。为了解决它,我们可能需要难以置信的大量的资源。同时我认为政治问题可能是其他问题的根源。举个例子,我们知道Windows 的GUI 风格和Java 的GUI 风格是不一样的,甚至不同版本的Windows 的GUI 风格也不一样。而且我们知道GUI 风格很不稳定,它发展演化得非常快,而且很容易被微软这样的大公司所引导和操纵。
Bjarne: 在这个问题上,资源(包括财力和人力)是一个关键问题,与“保持设计蓝图的一致性”同样重要。另外,即使我们已经有了这三样东西:蓝图、人力(数打甚至更多)、以及财力(至少要担负得起一些优秀的人在这个项目上的全职工作),这仍然会是个需要N 年才能完成的项目。而SUN 、微软、苹果以及其他主要竞争对手将会做些什么呢?毕竟包含了一个工业强度的GUI 库的ISO 标准C++ 对于它们的专有系统可不啻一记重击啊!我的猜测是它们大多会通过强大的市场手段来保护既得利益,标准委员会可不是为了在这种市场环境中呼风唤雨而组织起来的,我们至少还需要一个工业社团的支持以及和开源社群主力军的联盟。
荣耀: 在您看来,C++ 要想继续向前发展,除了开发一个更为广泛、更具威力的库以外,我们还应该做些什么?
Bjarne: 我 认为库方面的工作是关键。标准委员会没有足够的资源去创建一个全新的库,而且无论如何“让委员会设计”都不是个好主意。如果人们创建了新的库,在紧密相关 的领域把它们的努力融合起来以达到冲突最小,并且文献记录良好(并非只记录细节,还有设计原则),那么他们也许可以把这些东西带到委员会来,并有希望让它 们成为标准。如果没有成为标准的话,至少他们的努力有最大的机会成为“准”标准。有此打算的人应该把C++ 真正当作“C++ ”(使用继承、模板以及异常等)来看待,而不是从其它特性不是那么丰富的语言中抄袭设计而来。如果一个库的设计被发现没有用C++ 最佳地表达出来,那么它被接受为标准的几率微乎其微。
另一个可作贡献的领域是对技术的开发和推广。C++ 往往被以“很不理想”的方式使用着,MFC 就是一个典型的例子,它甚至还达不到80 年代中期对一个良好的OO 设 计的看法!我所说的“不理想”,是指没有达到它所能达到的可维护性的设计(通常这是由于对设计决策的糟糕的分解、低劣的封装以及对概念的拙劣表达而造成 的),而并非指在外部压力下要尽快把项目赶出来的个体程序员或团队的“不理想”。通常,一个较好的设计和现有的实践是背道而驰的,后者往往需要立即付出代 价(往往在很短时间内)。显然,任何显著的改进都需要在“一切照常”的基础上,并且至少追加学习所需的代价以及时间上的延迟。
这就把我们带到了最重要并且也许是最明显的改进实践的途径面前:教育。我们对编程和设计的教育必须比当前做得更好。新的语言特性、新技术以及新库碰到了不能理解并使用它们的人仍然是无用之刃。遗憾的是,我们恰恰缺乏优秀的C++ 入门书籍。Francis Glassborow 的新书《You Can Do It 》(译注:中文版《C++ 编程你也行》即将由人民邮电出版社出版) 和Koenig & Moo 的《Accelerated C++ 》是打破旧式而令人厌烦的教育方式的例子。那种教育方式把C++ 当作一个“稍微好一点的C ” 或者一门在实现“真正的面向对象”方面失败的语言。前者倾向于用一大堆语言技术细节来迷糊和恼怒读者,偏离了学习好的编程及设计技术的正确道路。后者通常 除了存在这些问题之外,还没能够教会在性能攸关的领域里编程的必要技术,而且没有让人感受到静态类型系统的价值。尤其遗憾的是,Glassborow 和Koenig & Moo 的书的风格都不是编程入门教材的传统风格,这也阻碍了它们的广泛普及。
我自己的两本书《C++ 程序设计语言》和《C++ 语言的设计与演化》的目标读者则是已经知道如何去编程然而不知道如何使用C++ 的人。这两本书确实满足了这部分读者,但我们要做的还不止这些。
任何时候只要有可能,程序员都可以通过将程序的主要部分用ISO C++ 来写,并将系统依赖性封装起来,从而来支持标准。也就是说,把系统依赖性限定到特定的区域,并通过以标准C++ 表 达的接口去访问它们。通常这并不容易做到,因为厂商总是怂恿程序员在代码中使用专有的、排他性的特性,但这就影响了可移植性,从而使程序移植到其他系统上 非常困难。然而,站在应用构建者的立场来说,从长远来看,挣脱厂商的束缚、维持可移植性是一件好事,这种隔离系统依赖性的努力终将从经济和技术两方面都得 到回报。
荣耀: 根据您掌握的资料,模板和泛型编程在业界被广泛采用了吗?还是主要局限于库作者?您认为对于普通程序员(而非库作者)来说,面向对象和泛型编程哪一个更重要?为什么?您认为今后模板和泛型编程应该比今天得到更普遍的应用吗?
Bjarne: “加法和乘法哪个更重要?”大多数情况下这是个愚蠢的问题。同理类推,“面向对象编程和泛型编程哪个更重要?”的问题也毫无意义。关键在于,它们都是基础性的东西,并且是互补的。对于许多问题而言,最佳解决方案往往要求将它们结合运用。
从理论的角度来说,你是在“ad-hoc 多态”和“参数化多态”之间做出选择。而从实现角度来说,则是在“运行期多态”和“编译期多态”之间进行选择。你必须对两者都有所了解,并且恰当地使用它们。和“ad-hoc 多态”(在C++ 中以类继承来表达)相比,“参数化多态”(在C++ 中以模板来表达)更具规则性,更利于逻辑或抽象思考。这就是为什么它被称为“ad-hoc ”、为什么模板代码通常具有更好的性能、以及为什么当个体表达式和语句的性能很重要时我们应该考虑泛型编程的原因之所在。相反,类继承则能够在分离式编译和维护的代码块之间提供更为明晰的接口。
(译注:“ad-hoc ”是“专门”的意思。事实上,多态一般分为“ad-hoc 多态”和“universal 多态”,前者一般指重载,后者一般指“参数化多态”(模板)或“包含多态”(类继承)。你可以从以下链接找到详细的解释:http://www.javaworld.com/javaworld/jw-04-2001/jw-0413-polymorph.html 。)
我不能说 哪个将会变得更重要,或者哪个应该更重要。那就好像是在加法和乘法之间偏袒某一方一样。但是我怀疑,和没有充分使用类继承和面向对象编程的人相比,有更多 的人没有充分使用模板和泛型编程。因此,我更多的时候鼓励人们考虑并使用泛型编程而不是面向对象编程。我确信我们将会看到越来越多的泛型编程应用。泛型编 程目前仍然没有被足够的理解和充分的使用。我们接受了近十年的面向对象的耳濡目染,所以我的感觉是面向对象常常被滥用了。值得注意的是,无论如何,泛型编 程和面向对象编程这两种技术/ 风格都比使用一团乱麻似的选择语句、单薄的数据结构以及指针来解决问题要强得多。我们的目标应该是在代码中更为直接和优雅地表达思想,模板和类继承只是为了达到这个目的的工具。
我的感觉是许多“普通的程序员”确实使用了模板,而不仅仅是一些“库设计”方面的精英。当然,我们自己写的模板代码可能要比基础库里的代码简单一些,这很自然,因为对于其它编程技术来说,也存在同样的现象。
荣耀: 我想知道您对模板元编程的看法,您甚至选编了一本模板元编程的书。
Bjarne: 我想我并不愿意把泛型编程和模板元编程区别开来,它们的区别仅仅是在层次和侧重点上。我通常倾向于把这一块统称为泛型编程。事实上我认为模板元编程是一个非常重要的领域,而目前的C++ 对它的支持却不佳,以至于在生成的代码中它并不能发挥应有的潜力。我认为人们在这一领域所作的许多努力是实验性的,其中许多理应获得成功,因为比起替代方案来,它们能使代码更清晰地表达基础概念,并且具有更好的性能以及可维护性。然而C++98 对这些技术的支持却不是很好,所以它们目前还不能成为主流。鉴于此,我在语言的改革方面所作的许多工作都跟泛型编程直接或间接有关。concept (用于分离式检查模板的使用和定义,以及用于更好地重载模板等),更好的初始化,以及更少的不规则性,都是对此有所帮助的努力,同样,用于支持标准库的设施(例如type traits )也会带来帮助。
荣耀: 我个人认为标准C++ 流库是面向对象和泛型编程结合运用的典范,您赞成这一点吗?对于准备尝试混合使用面向对象和泛型编程技术的程序员,您有什么建议或忠告?
Bjarne: 不, 我认为流输入输出流是一个不错的早期尝试。然而,它仅仅是一个非常初步的尝试而已,随着时间的推移而显得过于精致而复杂(正如发生在大多数成功的系统中的 那样)。我们现在可以做得更好。在我设计第一个流库时我意识到泛型编程的必要性,但是当时我并没有料到泛型编程最终会变成C++ 中如此重要的一个组成部分。
荣耀: 我必须得承认我也许太闭目塞听了,我真的没有看到过比标准C++ 输入输出流更好的面向对象编程和泛型编程的结合应用范例。您能给我一些线索吗?更重要的是,您能告诉我们有哪些是结合运用面向对象编程和泛型编程的最佳场合?谢谢!
Bjarne: 如果你面对的问题既需要某些运行期决议(需要面向对象编程),又具有一些能够从编译期决议中获益的方面(泛型编程的用武之地)的话,那么你就需要将面向对象编程和泛型编程结合起来。例如,面向对象编程的经典例子 — 将一个保存了shape 的容器中的所有元素都显示出来就属于这类问题。几十年前我第一次在Simula 中看到过这个例子,后来直到遇到了泛型编程,我才看到它的改进实现。考虑以下代码:
void draw_all(vector< Shape*> & vs)
{
for (int i=0; i < vs.size(); ++i) vs[i]->draw();
}
我猜想这并不能被看作纯粹的面向对象编程,因为我直接利用了“vs 是一个装有Shape* 元素的vector ”这个事实。毕竟,类型的参数化通常是被认为属于泛型编程的范畴。我们也可以消除这种对静态类型信息的使用(所谓“不纯粹的面向对象编程”):
void draw_all(Object* container)
{
Vector* v = dynamic_cast< Vector*> (container);
for (int i=0; i
但凡鼓励以上这种风格的语言,其语法通常都比较漂亮,然而这个例子却说明了当你把静态类型信息的使用减至最小的时候发生了什么。如今,在C++ 或其它语言中,仍然有人在使用这种风格。我只是希望他们在错误处理方面有系统化的准备。
在前一个例子中,vector< Shap*> 依赖于对泛型编程的一个最简单的运用:vector 的元素类型被参数化了,而且我们的示例代码正获益于此。在这个方向上我们还可以走得更远,即推而广之到所有标准库容器身上:
template< class Container> void draw_all(Container& cs)
{
for (typename C::iterator p=cs.begin(); p!=cs.end(); ++p)
(*p)->draw();
}
例如,这段代码就既可以作用于vector 上,又可以作用于list 上。编译期决议确保我们不用为这种泛化处理付出任何运行期额外代价。我们还可以通过在draw_all() 的使用接口中运用迭代器,从而进行进一步的泛化处理:
template< class Iter> void draw_all(Iter fist, Iter last)
{
for (; first!=last; ++first)
(*first)->draw();
}
这就使内建数组类型都得到了支持:
Shape* a[max];
// 向a 中填充Shape* 类型的元素
draw_all(a,a+max);
我们还可以结合运用标准库算法for_each() 和函数适配器mem_fun() 来消除显式的循环:
template< class Iter> void draw_all(Iter fist, Iter last)
{
for_each(first, last, mem_fun(&Shape::draw);
}
在这些例子中,我们结合了面向对象(对虚函数draw() 的调用以及对类继承体系的假设)和泛型编程(参数化的容器和算法)技术。我看不出如果这两种编程风格(即所谓的“范型”)各自独立运用如何达到同样好的效果。这也是一个简单的“多范型编程”的例子。
我认为在设计和编程技术方面,我们还需要做更多的工作,以便确定出“关于何时采用哪种范型以及如何结合运用它们”的更为具体的规则。为此,我们还需要一个比“多范型编程”更好的名字。
注意,这也是一个关于编译错误信息变得可怕的例子,因为我们并没有显式地表达出我们的假设。例如,我们假设容器里的元素类型为Shape* ,然而在代码中,这个假设却相当隐晦。这种情况下我们可以使用约束类(此处为Point_to ):
template< class Iter> void draw_all(Iter fist, Iter last)
{
Points_to< Iter,Shape*> ();
for_each(first, last, mem_fun(&Shape::draw);
}
然而我们又确实很想说明“first 和last 必须为前向迭代器”:
template< Forward_iterator< Shape*> Iter>
void draw_all(Iter fist, Iter last)
{
for_each(first, last, mem_fun(&Shape::draw);
}
这是“concepts ”可以大展拳脚的地方之一。
荣耀: 请原谅我重复一个老俗套问题。由于Java 和C# 今天都已经大获成功,您对Java 和C# 曾经的看法今天有无改变?我个人认为,与其说Java 和C# 的成功是语言自身的成功,还不如说是SUN 的Java 战略和微软的.NET 战略的成功。
Bjarne: 在对它们的本质技术优点以及对它们的市场能量的估计上面,我都是正确的。低估市场的影响是不明智的,尤其当它背后有价值上百万美元的“免费”库所支持的时候。Java 和C# 是不坏的语言,而且SUN 及其盟友以及微软及其盟友为其(过分夸张)的宣称提供重大的库和工具的支持。不过,这么说并不意味着我比喜欢C++ 更喜欢它们,对于要求严苛的应用而言更是如此。
荣耀: 您自觉或自发地使用过GOF 描述的设计模式了吗?您对设计模式怎么看?您对Loki 库中采用模板技术描述的静态设计模式怎么看?
Bjarne: 我并不喜欢根据特定的具名模式去思考,但我知道并且通常会使用这些在《设计模式》中描述的技术。顺便一提,《设计模式》是一本经典书籍,人们在搜寻最近最好的信息时不应该忘了这本书。其中的许多模式对于好的设计来说非常重要 — 给它们起什么名字倒无所谓。甚至你在TC++PL 中也能够找到一些有关它们的运用。要想了解在某个特定领域中对模式有意识且系统的运用,可以参考“深入C++ 系列”中Schmit 和Hunston 的两本关于ACE 的书。
除了明显的强大能力之外,我认为模式有两大弱点:
-
它倾向于鼓励“精致的专用术语”,这会阻碍新手的学习。
-
如果没有具体的“工具”支持,要想把一个思想广泛地传播到应用中是极其困难的。
例如,一 个优秀的库本身携带了很多优秀的思想,并允许程序员(和设计者)直接利用这些思想在库中的实现品来工作。而模式只是对某个思想(或一系列互相关联的思想) 的尽量一般性的描述,并刻意避免将这些思想作为库实现出来而招致的特殊性。这就导致了这些思想在传播上的问题,以及从代码中如何识别出模式的问题 — 特别是在代码被维护修改过之后。同样,要想从抽象层面上来理解一个模式也是非常困难的。在某个模式的抽象描述之后的实例代码进入我的视野之前,我倾向于对 自己的理解持保留态度。我见到过一些人,他们认为自己是在使用某个模式,而实际上做的却是该模式被设计用来避免的事情。这些都说明思想的传授可能会异常困 难。
可以让模式更具有可利用性的方式之一是为某些特定的环境提供模式的库的实现。Andrei Alexandrescu 的书和他的Loki 库可以被看成一次寻求结合高灵活性和高效率(和手写代码一样高效)编程风格的尝试。而模板元编程在大多数情况下都符合这个描述,STL 亦 然。为了从这种非常一般性的参数化中获益,设计(或编码)抉择必须从运行期转移到编译期,从而程序才更容易在时间或速度上得到优化。遗憾的是,许多编译器 都不能很好地把握模板技术所提供的明显的优化机会,这通常是由于编译器过早地扔掉了类型信息,并试图去优化每一片代码,就好像它们是用弱类型风格的C 所编写的一样。