C 的回归
周末出差,去另一个城市给公司的一个项目解决点问题。回程去机场的路上,我用手机上 google reader 打发时间。第一眼就看到孟岩大大新的一篇:Linux之父话糙理不糙 。主题是 C 与 C++ 的语言之争。转到刘江的 blog 下读完了 Linux之父炮轰C++:糟糕程序员的垃圾语言 大呼过瘾。立刻把链接短信发给了几个朋友。
语言之争永远是火药味十足的话题。尤其是 C 和 C++ 的目标市场又有很高的重合性,C++ 程序员往往对C++ 其有着宗教般的虔诚。我想,今天我在自己的 blog 上继续这个战争,一定会换来更多的骂名。只不过这次 Linus 几句话真是说到我心坎里去了,不喊出来会憋坏的 :D
首先,给不熟悉我的朋友做一个技术背景的自我介绍:
我不是一个 Linux 的 fans ,虽然我今天对 Windows 也没有什么好感,但我的大部分工作还是在 WIndows 上做应用软件开发的,对 Windows 还算熟悉。现在我也用非 Windows 的系统,但那是一台 FreeBSD 的机器,不是 Linux 。
我自认为对 C++ 相当熟悉,精读过市面上能买到的关于 C++ 的大部分书籍,像 D&E of C++ 这样的经典还读了不只一遍。用 C++ 写过至少数十万行代码,阅读过 STL 的大部分源码,和 ACE / Boost 的一小部分。
曾经我是 C++ 的忠实粉丝,如果谁说 C++ 的不是,要么会选择跟他辩论到底,要么会对此人不屑一顾。
还有一点我认为非常重要:我第一次爱上 C++ 是 15 年前(1992 年),然后对其慢慢冷淡,回归 C 的怀抱。而到了 2000 年,我又一次爱上 C++ 。也就是说,从热爱 C++ 到否定它,在我的个人经历中,有过两次。不排除未来有第三次的可能,但这一点足可说明,否定 C++ 是出于一种理性的判断,而不是一种冲动。
写上这些,并非是想倚老卖老。我知道,想骂我的 C++ 程序员,更讨厌有人倚老卖老的数落 C++ 的不是。而且论资格,我顶多及的上 Linus 大大的一个零头,既然有老人在前撑腰,下面说话的底气就可以足一些了 :)
C 是 C++ 的一个子集(从 C99 开始已经不是了),用 C 能写出来的代码,C++ 一样可以写出来,然后可以完成的更好。
这是新手们自以为是的攻击武器。Linus 用了一个很恰当的理由做出反击:“你当然可以用任何语言编写糟糕的代码。但是,有些语言,尤其是带有一些心智(mental)包袱的语言本身就非常糟糕。”
没错,我最想说的就是这个。C++ 就是一个“带有一些心智(mental)包袱的语言”。这对软件设计的影响非常之大,没有经年的软件开发实践很难理解这一点。
从这一点上展开,把 ASM 和 C 比较的问题和 C 与 C++ 的比较相提并论就没有意义了。
接下来要找到的问题要点就是,C++ 比 C 多出来那些东西后,真的会带来心智包袱吗?这个问题不好回答。单纯从 C++ 语言特性的繁杂导致的不易掌握和误用这些角度是很难说服我自己的,更别说去说服那些比我聪明的多,刻苦的多的 C++ 程序员们。我自认为对所谓 C++ 的高级特性掌握的还是不错的,并运用在诸多实际项目中。他们相当有趣,在某种程度上也非常的有效。代码可以获得相当高的执行效率,并可以缩短编码的时间(更少的键击数),完成他们也有很大的成就感。
好了,让我再引用 Linus 的一句说到我心坎里的话。“字符串/内存管理根本无关紧要。这不是重要的部分,而且也不复杂。唯一真正重要的部分是设计。”
设计!这才是重中之重。
如果要说,这最近 10 年的程序员生涯我学会了什么?我认为,我比以前能设计出更好的代码了。能更准确的把握设计的坏味道。而对编程语言的掌握,对操作系统的熟悉,工作相关知识的了解等等。那些只是自然而然发生的事,那些是知识的积累,而非能力的提高。
“抽象”,“面向对象”,“设计模式”,这些重要吗?重要。对软件开发相当重要。但重要不是必要,执迷于“抽象”会使你离目标越来越远。当我们一次又一次的提取出事物的共性,建立起抽象层的时候,我们可能丢弃了真实。C++ 继承了 C 语言中“信任程序员”这一设计哲学,致力于让程序员在建立抽象层时,可以不做出额外的消耗。他的解决方式是提供尽可能多的语言工具和设计选择,任何一个都允许你在不用的时候不带来额外的性能损失。
这是一个美好的愿景:C++ 程序员指望可以建立强大的可复用的抽象层,面对世界上一切的具体应用。同时 CPU 执行序列在穿越这个坚厚的抽象层的过程中,居然可以以光速通过(通过抽象层没有额外的执行效率付出)。为此:C++ 社区创造了 STL ,创造了 Boost 。它们共同的关键词是:效率、复用。
再往上呢?另一个问题产生了:“——低效的抽象编程模型,可能在两年之后你会注意到有些抽象效果不怎么样,但是所有代码已经依赖于围绕它设计的‘漂亮’对象模型了,如果不重写应用程序,就无法改正。”这一段依旧是 Linus 语,我不停的引用,是因为我明白这一点,但是不能表达的更清楚。
使用 C++ 的程序员不断的强调复用性,却不断的需要重写代码。如果一段代码可以不被重写,那多半是因为对重写工程量的妥协。是的,其实我们可以用 C++ 的各种特性写出更好,更漂亮,更高效的代码。两年前的框架不那么完美,不是 C++ 语言的错,是两年前的我能力有限的缘故。但是因为需要改写的是设计框架,这意味着我们必须跟着变更已经完成的功能模块,或是加上桥接层。
的确,STL 和 Boost 都是世界顶尖程序员完成的。代码质量非常的高(当然,我对 Boost 的一部分持保留意见)。我不拿编译器兼容性和可移植性或是编译速度说事,虽然这些的确是现实问题,但不足以成为反对 C++ 基础类库的理由。
好好的用好 C++ 当然得用好 STL ,Boost 也应该认真考察一下。能够仔细读一下源码更好。合格的 C++ 程序员应该做这个。否则作为 C++ 程序员你就违背了 C++ 语言的设计哲学:C++ 信任了你,你就该对的起这种信任,搞清楚你写的每一行代码背后,机器都去干了什么。
但是,STL 过于庞大了,Boost 更加是。我不是抱怨阅读和学习它们的源码的难度和需要的时间和精力。正相反,我在学习它们的过程中充满了乐趣和感激之情。高手前辈透过这些高质量的代码教会了我很多东西。我隐隐担心的是,这么庞大的代码,它的设计不可能是永远正确的。两年之后,他们的设计肯定依旧正确,再两年还是的。但是我几乎敢肯定,放之更长远的时间来看,绝对会在某些设计领域发现其不是最佳的选择。到那一天,我们会选择修改吗?我想 C++ 社区会被迫选择妥协。但是,C++ 程序员心中会充满痛苦。
C 在这个问题上的抉择是不一样的。在效率问题上,C 程序里最令人担心的是函数调用的消耗。C++ 程序员最津津乐道的案例就是 std::sort 全面击败了 C 库中的 qsort 。C 语言的失败正在于多余的函数调用消耗。
但是,从一开始 C 就选择了承认函数调用的消耗,而这一点几乎是唯一。付出了这个代价后,设计失误导致的效率下降问题几乎总可以避免。C 和 C++ 都可以选择重写设计失败的部分,但不一样的是, C 程序员几乎可以不考虑妥协的问题。同样的是考虑极端效率的语言,C 语言坦然面对缺陷,才是真正的符合了 KISS 原则。
我对这个问题的见解,可以再引用 Linus 的一段话作为收场。“如果你想用更花哨的语言,C++绝对是最糟糕的选择。如果想要真正的高级特性,那就选择有垃圾回收或者好的系统集成的,而不是既缺乏C的简约(sparseness)又缺乏C的直接而且没有重要概念的高层绑定(high-level bindings to important concepts)的东西。”。这是我最近几年来一直坚持的观点:C++ 的发展,一定要补充对 GC 支持所需要的特性。
强调一下,我并不讨厌 C++ :) 。 C++ 的粉丝们可以随便骂我,但是不要带上阶级仇恨。
ps. 最近两年多,我在做一个游戏引擎的项目。这个项目现在是第三个版本了。第一个版本是用 C++ 实现的,但是没有用任何已存在的类库(包括 STL)。在第二个版本中,我去掉了所有使用 C++ 高级特性实现的部分,只使用了 C++ 基本特性实现所有。今年重写的第三个版本,全部换成 C 代码了。这个项目的发展,可以反应出我个人对 C/C++ 理解的心路过程。
Comments
用C++的人大部分都有一个心智上的问题,以至会抵触,鄙视一些自己不喜欢的东西。儿子太顽皮,东搞搞西搞搞,爸爸教训一下不务正业的儿子,儿子就不认爸爸了,甚至要把爸爸消灭,爸爸也很生气,发誓要给儿子更严厉的教训....
这是我看完这些消息的结论。
==============================
眼光很锐利啊!
Posted by: sdk | (176) June 5, 2008 07:43 AM
恩,去年我也参加了这场争论,
当时我是C++的忠实FANS,
好像我的CSDN留言还成为了C++帮的第一个,呵呵。
现在发现应该保持中立了。
C++确实有点让人尴尬。
学的时候很带劲,用的时候就有点郁闷了。
现在的目标是学好C++,用好C.
Posted by: pass86 | (175) April 24, 2008 10:50 PM
放不下所以选择回归
Posted by: cat | (174) April 2, 2008 11:19 PM
抽象很容易掩盖很多已存在的关系,而这些关系很有可能使产品做得更好,尤其在性能方面。
--------------------------
举个简单的例子:(1)system -> (*)processor
客户程序需要列出产品中所有的processor名,供用户选择,然后调用相应的processor功能。
设计1:c++设计的时候很容易就想到了抽象出processor概念,交互时客户程序得到的是一个IProcessor*。
设计2:c设计时则很容易形如:
int get_num_procs();
char* get_proc_name(int proc_id);
-〉这两个设计有一个差别,对象默认指向的是一段连续空间,而ID(或句柄)指向的则是一段离散空间。
设计1结果可能会导致诸如客户程序列名字时将创建整个对象等问题(虽然可以使用proxy将空间切割)。
而最后会发现,设计2将更易于修改,而用c的方式(c++当然也可以使用该方式)写出设计2的代码将更简约。
Posted by: rodder | (173) February 16, 2008 11:33 AM
话题已经贴近CTO之类的群体,扯得太远了。碰到这类话题忍不住就臆想和唠叨了这么多,实在不好意思。走了~
Posted by: hm | (172) February 14, 2008 02:34 PM
发现一篇blog文,文中对语言的理解也有自己的一番看法。
http://canonical.javaeye.com/blog/147064
Posted by: hm | (171) February 14, 2008 02:10 PM
按9楼的说法,用虚拟机来保持恒定的场景,我感觉还不够。如果再联合UML一起的话……感觉这样才是语言发展的方向。
Posted by: hm | (170) February 14, 2008 01:42 PM
啊~ 还连带想起了某人的观点:知识件!
将知识从计算机软件中解放出来,软件工程师提供软件平台,而知识工程师提供知识模块……
Posted by: hm | (169) February 14, 2008 01:04 PM
忽然想到范畴论,这种抽象数学似乎也可以作为发展计算机语言的理论基础~
Posted by: hm | (168) February 14, 2008 12:29 PM
呵呵,现在发现9楼和52楼的观点很闪亮,Knuth相关著作是再拿起的时机了,顺便翻翻形式语义学。
Posted by: Anonymous | (167) February 13, 2008 04:43 PM
只有完全没有二义性,才能真正达到“复用”,而只在某种程度上没有二义性,那么脱离该程度,就难以复用了。
弄懂细节,就是为了不产生二义性。语言在该程度上细节如此之多,则语言在该程度上的二义性也就非常多,那么可以说它失败了么?
Posted by: Anonymous | (166) February 13, 2008 12:12 PM
最后:
你认为,代码是写给人看的,还是机器看的呢?
代码能不能有二义性呢?
语言的发展需要新的词汇,而不同场景对相同词汇的解释自然会有不同。
就算是c,它在不同编译器上的解释也会如此。
也许,语言的发展必须依赖一个恒定的场景,有或者计算机语言公式还未成熟。
也许,c++……
ps:离散数学~看来我又要端起它了……
8~
Posted by: hm | (165) February 13, 2008 10:11 AM
这一次长考学到很多,多谢。
ps:
lisp么~
唉,计算机是替人做事的,没有心的情况下,死物毕竟是死物,即使告诉它这是什么样的世界……
Posted by: hm | (164) February 13, 2008 08:07 AM
垃圾回收的历史比 C 语言长的多。
lisp 已经有超过 50 年的历史了。
Posted by: Cloud | (163) February 12, 2008 04:32 PM
c的黄金时代有垃圾回收概念么?
Posted by: hm | (162) February 12, 2008 11:33 AM
设计灵感可以来源于生活,用c++可以带来这方面的灵感(产品里可以建立垃圾回收站,也可以出现出租屋)。c的设计灵感来源于哪里?
Posted by: hm | (161) February 12, 2008 11:11 AM
呵呵,再胡扯一句:想把对象“杀死”,自然会有意外发生。而目前的各种“杀死”对象的方法和当今世界某些情况不是很相似么~
Posted by: hm | (160) February 12, 2008 10:51 AM
呵呵,c与c++的根本区别在于设计理念不同,当真正明了语言的设计理念和产品的设计理念时,自然而然会做出正确的选择。
ps:我真正明了了么?我只认为,c++,全局函数体是一个场所(景),一个让对象们表演的舞台(世界?宇宙?)。
当然,对象必须都是“活”的(你想让他自然死亡,还是被其他对象杀死?你真正的把int当作“活”物来看待了么?)。
Posted by: hm | (159) February 12, 2008 10:39 AM
最后一提,c++的main与c的main完全不同。
Posted by: Anonymous | (158) February 11, 2008 09:21 PM
当然,也可以选择只与处理器交流,(C存在的根本就是如此)。但是,我还是想创造……
Posted by: Anonymous | (157) February 11, 2008 09:13 PM
孩子们在这个世界中是什么样的存在?将他们描述出来才是语言的真意。那么,你是想写记叙文、议论文还是散文呢?
Posted by: Anonymous | (156) February 11, 2008 08:24 PM
我只知道我要的是创造,而不是模拟这个世界。
Posted by: Anonymous | (155) February 11, 2008 07:45 PM
语言紧紧是一种实现逻辑的工具而已...工具当然有好坏之分,但好坏之分不是绝对的而是相对的,实现某种逻辑而选择什么样的工具只能根据实际出发.邓小平理论明写着的"实事求事",还有一句叫做"走具有中国特色的社会主义道路",写到这份上了,应该明白乍回事了.
Posted by: ioriliao | (154) January 1, 2008 12:27 PM
to Wesley:
首先道歉。我兴奋起来的时候就会说胡话,会变成疯狗咬人。对不起。
然后对于你的具体问题。小规模、不涉及架构的代码复用,我想了一下,还真是用模板来解决最好, C 的话就用宏也行得通。
Posted by: Atry | (153) December 12, 2007 03:27 AM
Hello, nice site :)
Posted by: Brin | (152) December 5, 2007 08:06 AM
C 的另一个可能的优势就是缺乏一些自动机制。
比如 C++ 可以自动调用构造函数、析构函数,可以跨越多层调用栈的异常。而 C 都做不到。
前者要求程序员写明构造顺序,后者要求程序员必须在每一层检测错误。这些做法,可以避免无意的行为,对于谨慎的程序员来说,就更可能控制代码质量。
Posted by: Atry | (151) October 18, 2007 03:49 PM
学习跟做项目是不同的。学习本身不能带有功利性。了解各个方面才能抓住本质。
我一直觉得编程、设计软件需要悟性,它不是天生具来的,不是靠别人说什么是正确的什么是错误的就可以减少获得它的难度。学习过程中,经历别人口中所谓正确的路和错误的路同等重要。
因为学习没有捷径,思考的历程才是唯一的重点。
Posted by: Cloud
| (150)
October 14, 2007 10:35 PM
对于初学者很迷茫,难道现在不该学习c++了 还是应该做c更适合?
Posted by: 迷茫 | (149) October 14, 2007 12:27 AM
高手如云,呵呵。
写文章,我还想回归到文言文呢!
可惜从小就学现代文,文言文对我实在是太难了,所以只好将就了。
Posted by: 愚人 | (148) October 5, 2007 10:09 PM
重要的是思想和设计
什么语言都是小问题
Posted by: Anonymous | (147) October 3, 2007 05:23 PM
继承关系是一种耦合度很高的关系,它与组合及一般化(genericity)一样,提供了OO中的一种基本方法,用以将不同的软件组件组合起来。一个类的实例同时也是那个类的所有的祖先的实例。为了保证面向对象设计的有效性,我们应该保存下这种关系的一致性。在子类中的每一次重新定义都应该与在其祖先类中的最初定义进行一致性检查。子类中应该保存下其祖先类的需求。如果存在着不能被保存的需求,就说明了系统的设计有错误,或者是在系统中此处使用继承是不恰当的。由于继承是面向对象设计的基础,所以才会要求有一致性检测。C++中对于非虚拟函数重载的实现, 意味着编译器将不会为其进行一致性检测。C++并没有提供面向对象设计的这方面的保证。
继承被分成"语法"继承和"语义"继承两部分。Saake等人将其描述如下:"语法继承表示为结构或方法定义的继承,并且因此与代码的重复使用(以及重写被继承方法的代码)联系起来。语义继承表示为对对象语义(即对象自己)的继承,。这种继承形式可以从语义的数据模型中被得知,在此它被用于代表在一个应用程序的若干个角色中出现的一个对象。"[SJE 91]。Saake等人集中研究了继承的语义形式。通过是行为还是语义的继承方式的判断,表示了对象在系统中所扮的角色。
然而, Wegner相信代码继承更具有实际的价值。他将语法与语义继承之间的区别表示为代码和行为上的区别[Weg 91](p43)。他认为这样的划分不会引起一方与另一方的兼容,并且还经常与另一方不一致。Wegner同样也提出这样的问题:"应该怎样抑制对继承属性的修改?"代码继承为模块化(modularisation)提供一个基础。行为继承则依赖于"is-a"关系。这两种继承方式在合适处都十分有用。它们都要求进行一致性的检测,这与实际上的有意义的继承密不可分。
看起来在语义保持关系中那些限制最多的形式中,继承似乎是其中最强的形式;子类应该保存祖先类中的所有假设。
Meyer [Meyer 96a and 96b]也对继承技术进行了分类。在他的分类法中,他指出了继承的12种用法。这些分析也给我们怎么使用继承提供了一个很好的判断标准,如:什么时候应该使用继承,什么时候不应该它。
软件组件就象七巧板一样。当我们组装七巧板时,每一块板的形状必须要合适,但更重要地是,最终拼出的图像必须要有意义,能够被说得通。而将软件组件组合起来就更困难了。七巧板只是需要将原本是完整的一幅图像重新组合起来。而对软件组件的组合会得到什么样的结果,是我们不可能预见到的。更糟的是,七巧板的每一块通常是由不同的程序员产生的,这样当整个的系统被组合起来时,对于它们的吻合程度的要求就更高了。
C++中的继承像是一块七巧板,所有的板块都能够组合在一起,但是编译器却没有办法检测最终的结果是否有意义。换句话说,C++仅为类和继承提供了语法,而非语义。可重用的C++函数库的缓慢出现,暗示了C++可能会尽可能地不支持可重用性。相反的是,Java,Eiffel和Object Pascal都与函数库包装在一起出现。Object Pascal与MacApp应用软件框架联系非常紧密。Java也从与Java API的耦合中解脱出来,取而代之的是一个包容广泛的函数库。Eiffel也同样是与一个极其全面的函数库集成在一起,该函数库甚至比Java的还要大。事实上函数库的概念已经成为一个优先于Eiffel语言本身的工程,用以对所有在计算机科学中通用的结构进行重新分类,得到一个常用的分类法。 [Meyer 94].
Posted by: hi | (146) September 27, 2007 02:14 PM
函数重载
C++允许在参数类型不同的前提下重载函数。重载的函数与具有多态性的函数(即虚函数)不同处在于:调用正确的被重载函数实体是在编译期间就被决定了的;而对于具有多态性的函数来说,是通过运行期间的动态绑定来调用我们想调用的那个函数实体。多态性是通过重定义(或重写)这种方式达成的。请不要被重载(overloading)和重写(overriding)所迷惑。重载是发生在两个或者是更多的函数具有相同的名字的情况下。区分它们的办法是通过检测它们的参数个数或者类型来实现的。重载与CLOS中的多重分发(multiple dispatching)不同,对于参数的多重分发是在运行期间多态完成的。
【Reade 89】中指出了重载与多态之间的不同。重载意味着在相同的上下文中使用相同的名字代替出不同的函数实体(它们之间具有完全不同的定义和参数类型)。多态则只具有一个定义体,并且所有的类型都是由一种最基本的类型派生出的子类型。C. Strachey指出,多态是一种参数化的多态,而重载则是一种特殊的多态。用以判断不同的重载函数的机制就是函数标示(function signature)。
重载在下面的例子中显得很有用:
max( int, int )
max( real, real )
这将确保相对于类型int和real的最佳的max函数实体被调用。但是,面向对象的程序设计为该函数提供了一个变量,对象本身被被当作一个隐藏的参数传递给了函数(在C++中,我们把它称为this)。由于这样,在面向对象的概念中又隐式地包含了一种对等的但却更有更多限制的形式。对于上述讨论的一个简单例子如下:
int i, j;
real r, s;
i.max(j);
r.max(s);
但如果我们这样写:i.max(r),或是r.max(j),编译器将会告诉我们在这其中存在着类型不匹配的错误。当然,通过重载运算符的操作,这样的行为是可以被更好地表达如下:
i max j 或者
r max s
但是,min和max都是特殊的函数,它们可以接受两个或者更多的同一类型的参数,并且还可以作用在任意长度的数组上。因此,在Eiffel中,对于这种情况最常见的代码形式看起来就像这样:
上面的例子显示,面向对象的编程典范(paradigm),特别是和范型化(genericity)结合在一起时,也可以达到函数重载的效果而不需要C+ +中的函数重载那样的声明形式。然而是C++使得这种概念更加一般化。C++这样作的好处在于,我们可以通过不止一个的参数来达到重载的目的,而不是仅使用一个隐藏的当前对象作为参数这样的形式。
另外一个我们需要考虑的因素是,决定(resolved)哪个重载函数被调用是在编译阶段完成的事情,但对于重写来说则推后到了运行期间。这样看起来好像重载能够使我们获得更多性能上的好处。然而,在全局分析的过程中编译器可以检测函数min 和max是否处在继承的最末端,然后就可以直接的调用它们(如果是的话)。这也就是说,编译器检查到了对象i和r,然后分析对应于它们的max函数,发现在这种情况下没有任何多态性被包含在内,于是就为上面的语句产生了直接调用max的目标代码。与此相反的是,如果对象n被定义为一个NUMBER, NUMBER又提供一个抽象的max函数声明(我们所用的REAL.max和INTERGER.max都是从它继承来的),那么编译器将会为此产生动态绑定的代码。这是因为n既可能是INTEGER,也有可能是REAL。
现在你是不是觉得C++的这种方法(即通过提供不同的参数来实现函数的重载)很有用?不过你还必须明白,面向对象的程序设计对此有着种种的限制,存在着许多的规则。C++是通过指定参数必须与基类相符合的方式实现它的。传入函数中的参数只能是基类,或是基类的派生类。例如:
其中d必须与类'B'相符,编译器会检测这些。
通过不同的函数签名(signature)来实现函数重载的另一种可行的方法是,给不同的函数以不同的名字,以此来使得它们的签名不同。我们应该使用名字来作为区分不同实体(entities)的基础。编译器可以交叉检测我们提供的实参是否符合于指定的函数需要的形参。这同时也导致了软件更好的自记录(self-document)。从相似的名字选择出一个给指定的实体通常都不会很容易,但它的好处确实值得我们这样去做。
[Wiener95]中提供了一个例子用以展示重载虚拟函数可能出现的问题:
class Parent { public: virutal int doIt( int v ) { return v * v; } }; class Child: public Parent { public: int doIt( int v, int av = 20 ) { return v * av; } }; int main() { int i; Parent *p = new Child(); i = p->doIt(3); return 0; }当程序执行完后i会等于多少呢?有人可能会认为是60,然而结果却是9。这是因为在Child中doIt的签名与在Parent中的不一致,它并没有重写Parent中的doIt,而仅仅是重载了它,在这种情况下,缺省值没有任何作用。
Java也提供了方法重载,不同的方法可以拥有同样的名字及不同的签名。
在Eiffel中没有引入新的技术,而是使用范型化、继承及重定义等。Eiffel提供了协变式的签名方式,这意味着在子类的函数中不需要完全符合父类中的签名,但是通过Eiffel的强类型检测技术可以使得它们彼此相匹配。
Posted by: hi | (145) September 27, 2007 02:13 PM
保证类型安全的联结属性(type-safe linkage)
C++ARM中解释说type-safe linkage并不能100%的保证类型安全。既然它不那100%的保证类型安全,那么它就肯定是不安全的。统计分析显示:即便在很苛刻的情况下,C++ 出现单独的O-ring错误的可能性也只有0.3%。但我们一旦将6种这样的可能导致出错的情况联合起来放在一起,出错的几率就变得大为可观了。在软件中,我们经常能够看到一些错误的起因就是其怪异的联合。OO的一个主要目的就是要减少这种奇怪的联合出现。
大多数问题的起因都是一些难以察觉的错误,而不是那些简单明了的错误导致问题的产生。而且在通常的情况下,不到真正的临界时期,这样的错误一般都很难被检测到,但我们不能由此就低估了这种情况的严肃性。有许多的计划都依赖于其操作的正确性,如太空计划、财政结算等。在这些计划中采用不安全的解决方案是一种不负责任的做法,我们应该严厉禁止类似情况的出现。
C++在type-safe linkage上相对于C来说有了巨大的进步。在C中,链接器可以将一个带有参数的诸如f(p1,...)这样的函数链接到任意的函数f()上面,而这个 f()甚至可以没有参数或是带有不同的参数都行。这将会导致程序在运行时出错。由于C++的type-safe linkage机制是一种在链接器上实做的技巧,对于这样的不一致性,C++将统统拒绝。
C++ARM将这样的情况概括如下--“处理所有的不一致性->这将使得C++得以100%的保证类型安全->这将要求对链接器的支持或是机制(环境)能够允许编译器访问在其他编译单元里面的信息”。
那么为什么市面上的C++编译器(至少AT&T的是如此)不提供访问其他毕业单元中的信息的能力呢?为什么到现在也没有一种特殊的专门为C++设计的链接器出现,可以100%的保证类型安全呢?答案是C++缺乏一种全局分析的能力(在上一节中我们讨论过)。另外,在已有的程序组件外构造我们的系统已经是一种通用的Unix软件开发方式,这实现了一定的重用,然而它并不能为面向对象方式的重用提供真正的弹性及一致性。
在将来, Unix可能会被面向对象的操作系统给替代,这样的操作系统足够的“开放”并且能够被合适地裁剪用以符合我们的需求。通过使用管道(pipe)及标志 (flag),Unix下的软件组件可以被重复利用以提供所需的近似功能。这种方法在一定的情况下行之有效,并且颇负效率(如小型的内部应用,或是用以进行快速原型研究),但对于大规模、昂贵的、或是对于安全性要求很高的应用来说,采取这样的开发方法就不再适合了。在过去的十年中,集成的软件(即不采用外部组件开发的软件)的优点已经得到了认同。传统的Unix系统不能提供这样的优点。相比而言,集成的系统更加的复杂,对于开发它们的开发人员有着更多的要求,但是最终用户(end user)要求的就是这样的软件。将所有的东西拙劣的放置于一起构成的系统是不可接受的。现在,软件开发的重心已经转到组件式软件开发上面来了,如公共领域的OpenDoc或是Microsoft的OLE。
对于链接来说,更进一步的问题出现在:不同的编译单元和链接系统可能会使用不同的名字编码方式。这个问题和type-safe linkage有关,不过我们将会在“重用性及兼容性”这节讲述之。
Java使用了一种不同的动态链接机制,这种机制被设计的很好,没有使用到Unix的链接器。Eiffel则不依赖于Unix或是其他平台上的链接器来检测这些问题,一切都由编译器完成。
Eiffel 定义了一种系统层上的有效性(system-level validity)。一个Eiffel编译器也就因此需要进行封闭环境下的分析,而不是依赖于链接器上的技巧。你也可以就此认为Eiffel程序能够保证 100%的类型安全。对于Eiffel来说有一个缺点就是,编译器需要干的事情太多了。(通常我们会说的是它太“慢”了,但这不够精确)目前我们可以通过对于Eiffel提供一定的扩展来解决这个问题,如融冰技术(melting-ice technology),它可以使得我们对于系统的改动和测试可以在不需要每次都进行重新编译的情况下进行。
现在让我们来概括一下前两个小节 - 有两个原因使我们需要进行全局(或封闭环境下的)分析:一致性检测及优化。这样做可以减掉程序员身上大量的负担,而缺乏它是C++中的一个很大的不足。
Posted by: hi | (144) September 27, 2007 02:11 PM
全局分析
【P&S 94】中提到对于类型安全的检测来说有两种假设。一种是封闭式环境下的假设,此时程序中的各个部分在编译期间就能被确定,然后我们可以对于整个程序来进行类型检测。另一种是开放式环境下的假设,此时对于类型的检测是在单独的模块中进行的。对于实际开发和建立原型来说,第二种假设显得十分有效。然而,【P&S 94】中又提到,“当一种已经完成的软件产品到达了成熟期时,采用封闭式环境下的假设就可以被考虑了,因为这样可以使得一些比较高级的编译技术得以有了用武之处。只有在整个程序都被了解的情况下,我们才可能在其上面执行诸如全局寄存器分配、程序流程分析及无效代码检测等动作。”(附:【P&S 94】Jens Palsberg and Michael I. Schwartzbach, Object-Oriented Type Systems, Wiley 1994)
C++中的一个主要问题就是:对于程序的分析过程被编译器(工作于开放式环境下的假设)和链接器(依赖于十分有限的封闭式环境下的分析)给划分开了。封闭式环境下的或是全局的分析被采用的实质原因有两个方面:首先,它可以保证汇编系统的一致性;其次,它通过提供自动优化,减轻了程序员的负担。
程序员能够被减轻的主要负担是:设计父类的程序员不再需要(不得不)通过利用虚拟函数的修饰成份(virtual),来协助编译器建立起vtable。正如我们在“虚拟函数”中所说,这样做将会影响到软件的弹性。Vtable不应该在一个单独的类被编译时就被建立起来,最好是在整个系统被装配在一起时一并被建立。在系统被装配(链接)时期,编译器和链接器协同起来,就可以完全决定一个函数是否需要在vtable中占有一席之地。除上述之外,程序员还可以自由地使用在其他模块中定义的一些在本地不可见的信息;并且程序员不再需要维护头文件的存在了。
在Eiffel和Object Pascal中,全局分析被应用于整个系统中,决定真正的多态性的函数调用,并且构造所需的vtable。在Eiffel中,这些是由编译器完成的。在 Object Pascal中,Apple扩展了链接器的功能,使之具有全局分析的能力。这样的全局分析在C/Unix环境下很难被实现,所以在C++中,它也没有被包含进去,使得负担被留给了程序员。
为了将这个负担从程序员身上移除,我们应该将全局分析的功能内置于链接器中。然而,由于C++一开始的版本是作为一个Cfront预处理器实现的,对于链接器所做的任何必要的改动不能得到保证。C++的最初实现版本看起来就像一个拼凑起来的东西,到处充满着漏洞。【译者认为:这也太过分了吧:)】C++的设计严格地受限于其实现技术,而不是其他(例如没有采用好的程序语言设计原理等),因为那样就需要新的编译器和链接器了。也就是说,现在的C++发展严格地受限于其最初的试验性质的产品。
我现在确信这种技术上的依赖关系(即C++ 依赖于早先的C)严重地损害了C++,使之不是一个完整意义上的面向对象的高级语言。一个高级语言可以将簿记工作从程序员身上接手过去,交给编译器去完成,这也是高级语言的主要目的。缺乏全局(或是封闭式环境下的)分析是C++的一个主要不足,这使得C++在和Eiffel之类的语言相比时显得十分地不足。由于Eiffel坚持系统层次上的有效性及全局分析,这意味着Eiffel要比C++显得有雄心多了,但这也是Eiffel产品为什么出现地这么缓慢的主要原因。
Java只有在需要时才动态地载入软件的部分,并将它们链接起来成为一个可以运行的系统。也因而使得静态的编译期间的全局分析变成不可能的了(因为Java被设计成为一个动态的语言)。然而,Java假设所有的方法都是virtual的,这也就是为什么Java和 Eiffel是完全不同的工具的一个原因。关于Eiffel,可以参见于Dynamic Linking in Eiffel(DLE)
Posted by: hi | (143) September 27, 2007 02:10 PM
虚拟函数
在所有对C++的批评中,虚拟函数这一部分是最复杂的。这主要是由于C++中复杂的机制所引起的。虽然本篇文章认为多态(polymorphism)是实现面向对象编程(OOP)的关键特性,但还是请你不要对此观点(即虚拟函数机制是C++中的一大败笔)感到有什么不安,继续看下去,如果你仅仅想知道一个大概的话,那么你也可以跳过此节。【译者注:建议大家还是看看这节会比较好】
在C++中,当子类改写/重定义(override/redefine)了在父类中定义了的函数时,关键字virtual使得该函数具有了多态性,但是virtual关键字也并不是必不可少的(只要在父类中被定义一次就行了)。编译器通过产生动态分配(dynamic dispatch)的方式来实现真正的多态函数调用。
这样,在C++中,问题就产生了:如果设计父类的人员不能预见到子类可能会改写哪个函数,那么子类就不能使得这个函数具有多态性。这对于C++来说是一个很严重的缺陷,因为它减少了软件组件(software components)的弹性(flexibility),从而使得写出可重用及可扩展的函数库也变得困难起来。
C++同时也允许函数的重载(overload),在这种情况下,编译器通过传入的参数来进行正确的函数调用。在函数调用时所引用的实参类型必须吻合被重载的函数组(overloaded functions)中某一个函数的形参类型。重载函数与重写函数(具有多态性的函数)的不同之处在于:重载函数的调用是在编译期间就被决定了,而重写函数的调用则是在运行期间被决定的。
当一个父类被设计出来时,程序员只能猜测子类可能会重载/重写哪个函数。子类可以随时重载任何一个函数,但这种机制并不是多态。为了实现多态,设计父类的程序员必须指定一个函数为virtual,这样会告诉编译器在类的跳转表(class jump table)【译者窃以为是vtable,即虚拟函数入口表】中建立一个分发入口。于是,对于决定什么事情是由编译器自动完成,或是由其他语言的编译器自动完成这个重任就放到了程序员的肩上。这些都是从最初的C++的实现中继承下来的,而和一些特定的编译器及联结器无关。
对于重写,我们有着三种不同的选择,分别对应于:“千万别”,“可以”及“一定要”重写:
1、重写一个函数是被禁止的。子类必须使用已有的函数
2、函数可以被重写。子类可以使用已有的函数,也可以使用自己写的函数,前提是这个函数必须遵循最初的界面定义,而且实现的功能尽可能的少及完善
3、函数是一个抽象的函数。对于该函数没有提供任何的实现,每个子类都必须提供其各自的实现
父类的设计者必须要决定1和3中的函数,而子类的设计者只需要考虑2就行了。对于这些选择,程序语言必须要提供直接的语法支持。
选项1、
C ++并不能禁止在子类中重写一个函数。即使是被声明为private virtual的函数也可以被重写。【Sakkinen92】中指出了即使在通过其他方法都不能访问到private virtual函数,子类也可以对其进行重写。【译者注:Sakkinen92我也没看过,但经我简单的测试,确实可以在子类中重写父类中的 private virtual函数】
实现这种选择的唯一方法就是不要使用虚拟函数,但是这样的话,函数就等于整个被替换掉了。首先,函数可能会在无意中被子类的函数给替换掉。在同一个scope中重新宣告一个函数将会导致名字冲突(name clash);编译器将会就此报告出一个“duplicate declaration”的语法错误。允许两个拥有同名的实体存在于同一个scope中将会导致语义的二义性(ambiguity)及其他问题(可参见于 name overloading这节)。
下面的例子阐明了第二个问题:
class A { public: void nonvirt(); virtual void virt(); }; class B : public A { public: void nonvirt(); void virt(); }; A a; B b; A *ap = &b; B *bp = &b; bp->nonvirt(); file://calls B::nonvirt as you would expect ap->nonvirt(); file://calls A::nonvirt even though this object is of type B ap->virt(); file://calls B::virt, the correct version of the routine for B objects在这个例子里,B扩展或替换掉了A中的函数。B::nonvirt是应该被B的对象调用的函数。在此处我们必须指出,C++给客户端程序员(即使用我们这套继承体系架构的程序员)足够的弹性来调用A::nonvirt或是B::nonvirt,但我们也可以提供一种更简单,更直接的方式:提供给A:: nonvirt和B::nonvirt不同的名字。这可以使得程序员能够正确地,显式地调用想要调用的函数,而不是陷入了上面的那种晦涩的,容易导致错误的陷阱中去。具体方法如下:
class B: public A { public: void b_nonvirt(); void virt(); } B b; B *bp = &b; bp->nonvirt(); file://calls A::nonvirt bp->b_nonvirt(); file://calls B::b_nonvirt现在,B的设计者就可以直接的操纵B的接口了。程序要求B的客户端(即调用B的代码)能够同时调用A::nonvirt和B::nonvirt,这点我们也做到了。就Object-Oriented Design(OOD)来说,这是一个不错的做法,因为它提供了健壮的接口定义(strongly defined interface)【译者认为:即不会引起调用歧义的接口】。C++允许客户端程序员在类的接口处卖弄他们的技巧,借以对类进行扩展。在上例中所出现的就是设计B的程序员不能阻止其他程序员调用A::nonvirt。类B的对象拥有它们自己的nonvirt,但是即便如此,B的设计者也不能保证通过B的接口就一定能调用到正确版本的nonvirt。
C++同样不能阻止系统中对其他处的改动不会影响到B。假设我们需要写一个类C,在C 中我们要求nonvirt是一个虚拟的函数。于是我们就必须回到A中将nonvirt改为虚拟的。但这又将使得我们对于B::nonvirt所玩弄的技巧又失去了作用(想想看,为什么:D)。对于C需要一个virtual的需求(将已有的nonvirtual改为virtual)使得我们改变了父类,这又使得所有从父类继承下来的子类也相应地有了改变。这已经违背了OOP拥有低耦合的类的理由,新的需求,改动应该只产生局部的影响,而不是改变系统中其他地方,从而潜在地破坏了系统的已有部分。
另一个问题是,同样的一条语句必须一直保持着同样的语义。例如:对于诸如a->f()这样的多态性语句的解释,系统调用的是由最符合a所真正指向类型的那个f(),而不管对象的类型到底是A,还是A的子类。然而,对于C++的程序员来说,他们必须要清楚地了解当f()被定义成virtual或是non-virtual时,a->f()的真正涵义。所以,语句a->f()不能独立于其实现,而且隐藏的实现原理也不是一成不变的。对于f()的宣告的一次改变将会相应地改变调用它时的语义。与实现独立意味着对于实现的改变不会改变语句的语义,或是执行的语义。
如果在宣告中的改变导致相应的语义的改变,编译器应该能检测到错误的产生。程序员应该在宣告被改变的情况下保持语义的不变。这反映了软件开发中的动态特性,在其中你将能发现程序文本的永久改变。
其他另一个与a->f()相应的,语义不能被保持不变的例子是:构造函数(可参考于C++ ARM, section 10.9c, p 232)。而Eiffel和Java则不存在这样的问题。它们中所采用的机制简单而又清晰,不会导致C++中所产生的那些令人吃惊的现象。在Java中,所有的一起都是虚拟的,为了让一个方法【译者注:对应于C++的函数】不能被重写,我们可以用final修饰符来修饰这个方法。
Eiffel允许程序员指定一个函数为frozen,在这种情况下,这个函数就不能在子类中被重写。
选项2、
是使用现有的函数还是重写一个,这应该是由撰写子类的程序员所决定的。在C++中,要想拥有这种能力则必须在父类中指定为virtual。对于OOD来说,你所决定不想作的与你所决定想作的同样重要,你的决定应该是越迟下越好。这种策略可以避免错误在系统前期就被包含进去。你作决定越早,你就越有可能被以后所证明是错误的假设所包围;或是你所作的假设在一种情况下是正确的,然而在另一种情况下却会出错,从而使得你所写出来的软件比较脆弱,不具有重用性(reusable)【译者注:软件的可重用性对于软件来说是一个很重要的特性,具体可以参考《Object-Oriented Software Construct》中对于软件的外部特性的叙述,P7, Reusability, Charpter 1.2 A REVIEW OF EXTERNAL FACTORS】。
C++要求我们在父类中就要指定可能的多态性(这可以通过virtual来指定),当然我们也可以在继承链中的中间的类导入virtual机制,从而预先判断某个函数是否可以在子类中被重定义。这种做法将导致问题的出现:如那些并非真正多态的函数(not actually polymorphic)也必须通过效率较低的table技术来被调用,而不像直接调用那个函数来的高效【译者注:在文章的上下文中并没有出现not actually polymorphic特性的确切定义,根据我的理解,应该是声明为polymorphic,而实际上的动作并没能体现polymorphic这样的一种特性】。虽然这样做并不会引起大量的花费(overhead),但我们知道,在OO程序中经常会出现使用大量的、短小的、目标单一明确的函数,如果将所有这些都累计下来,也会导致一个相当可观的花费。C++中的政策是这样的:需要被重定义的函数必须被声明为virtual。糟糕的是,C++同时也说了, non-virtual函数不能被重定义,这使得设计使用子类的程序员就无法对于这些函数拥有自己的控制权。【译者注:原作中此句显得有待推敲,原文是这样写的:it says that non-virtual routines cannot be redefined, 我猜测作者想表达的意思应该是:If you have defined a non-virtual routine in base, then it cannot be virtual in the base whether you redefined it as virtual in descendant.】
Rumbaugh等人对于C++中的虚拟机制的批评如下:C++拥有了简单实现继承及动态方法调用的特性,但一个C++的数据结构并不能自动成为面向对象的。方法调用决议(method resolution)以及在子类中重写一个函数操作的前提必须是这个函数/方法已经在父类中被声明为virtual。也就是说,必须在最初的类中我们就能预见到一个函数是否需要被重写。不幸的是,类的撰写者可能不会预期到需要定义一个特殊的子类,也可能不会知道那些操作将要在子类中被重写。这意味着当子类被定义时,我们经常需要回过头去修改我们的父类,并且使得对于通过创建子类来重用已有的库的限制极为严格,尤其是当这个库的源代码不能被获得是更是如此。(当然,你也可以将所有的操作都定义为virtual,并愿意为此付出一些小小的内存花费用于函数调用)【RBPEL91】
然而,让程序员来处理virtual是一个错误的机制。编译器应该能够检测到多态,并为此产生所必须的、潜在的实现virtual的代码。让程序员来决定 virtual与否对于程序员来说是增加了一个簿记工作的负担。这也就是为什么C++只能算是一种弱的面向对象语言(weak object-oriented language):因为程序员必须时刻注意着一些底层的细节(low level details),而这些本来可以由编译器自动处理的。
在C++中的另一个问题是错误的重写(mistaken overriding),父类中的函数可以在毫不知情的情况下被重写。编译器应该对于同一个名字空间中的重定义报错,除非编写子类的程序员指出他是有意这么做的(即对于虚函数的重写)。我们可以使用同一个名字,但是程序员必须清楚自己在干什么,并且显式地声明它,尤其是在将自己的程序与已经存在的程序组件组装成新的系统的情况下更要如此。除非程序员显式地重写已有的虚函数,否则编译器必须要给我们报告出现了名字被声明多处(duplicate declaration)的错误。然而,C++却采用了Simula最初的做法,而这种方法到现在已经得到了改良。其他的一些程序语言通过采用了更好的、更加显式的方法,避免了错误重定义的出现。
解决方法就是virtual不应该在父类中就被指定好。当我们需要运行时的动态绑定时,我们就在子类中指定需要对某个函数进行重写。这样做的好处在于:对于具有多态性的函数,编译器可以检测其函数签名(function signature)的一致性;而对于重载的函数,其函数签名在某些方面本来就不一样。第二个好处表现在,在程序的维护阶段,能够清楚地表达程序的最初意愿。而实际上后来的程序员却经常要猜测先前的程序员是不是犯了什么错误,选择一个相同的名字,还是他本来就想重载这个函数。
在 Java中,没有virtual这个关键字,所有的方法在底层都是多态的。当方法被定义为static, private或是final时,Java直接调用它们而不是通过动态的查表的方式。这意味着在需要被动态调用时,它们却是非多态性的函数,Java的这种动态特性使得编译器难以进行进一步的优化。
Eiffel和Object Pascal迎合了这个选项。在它们中,编写子类的程序员必须指定他们所想进行的重定义动作。我们可以从这种做法中得到巨大的好处:对于以后将要阅读这些程序的人及程序的将来维护者来说,可以很容易地找出来被重写的函数。因而选项2最好是在子类中被实现。
Eiffel和Object Pascal都优化了函数调用的方式:因为他们只需要产生那些真正多态的函数的调用分配表的入口项。对于怎样做,我们将会在global analysis这节中讨论。
选项3、
纯虚函数这样的做法迎合了让一个函数成为抽象的,从而子类在实例化时必须为其提供一个实现这样的一个条件。没有重写这些函数的任何子类同样也是抽象类。这个概念没有错,但是请你看一看pure virtual functions这一节,我们将在那节中对于这种术语及语法进行批判讨论。
Java也拥有纯虚方法(同样Eiffel也有),实现方法是为该方法加上deffered标注。
结论:
virtual 的主要问题在于,它强迫编写父类的程序员必须要猜测函数在子类中是否有多态性。如果这个需求没有被预见到,或是为了优化、避免动态调用而没有被包含进去的话,那么导致的可能性就是极大的封闭,胜过了开放。在C++的实现中,virtual提高了重写的耦合性,导致了一种容易产生错误的联合。
Virtual 是一种难以掌握的语法,相关的诸如多态、动态绑定、重定义以及重写等概念由于面向于问题域本身,掌握起来就相对容易多了。虚拟函数的这种实现机制要求编译器为其在class中建立起virtual table入口,而global analysis并不是由编译器完成的,所以一切的重担都压在了程序员的肩上了。多态是目的,虚拟机制就是手段。Smalltalk, Objective-C, Java和Eiffel都是使用其他的一种不同的方法来实现多态的。
Virtual是一个例子,展示了C ++在OOP的概念上的混沌不清。程序员必须了解一些底层的概念,甚至要超过了解那些高层次的面向对象的概念。Virtual把优化留给了程序员;其他的方法则是由编译器来优化函数的动态调用,这样做可以将那些不需要被动态调用的分配(即不需要在动态调用表中存在入口)100%地消除掉。对于底层机制,感兴趣的应该是那些理论家及编译器实现者,一般的从业者则没有必要去理解它们,或是通过使用它们来搞清楚高层的概念。在实践中不得不使用它们是一件单调乏味的事情,并且还容易导致出错,这阻止了软件在底层技术及运行机制下(参见并发程序)的更好适应,降低了软件的弹性及可重用性。
Posted by: hi | (142) September 27, 2007 02:08 PM
对于“潜在陷阱”的“警告”是C++著作的一大特色:BjarnecStroustrup的书中就包含了许多这样的“告诫”,而ScottcMeyers的书则给出了更多.他们这样为C++辩护:只要你知道了需要避免的问题,那么万事无恙.
然而警告并不够,语言本身就应该被设计得更为健壮.购车的人可不会对汽车销售商的“警告和劝诫”买账——你听过谁这样宣传自己的汽车吗?“警告:本车型驾驶途中偶尔会向左猛偏,如果从Amalfi开往Sorrento,它会爆炸……”.
市场炒作(hype)的一大来源是人们常常和“最新技术”闪电恋爱.我们深深地迷恋计算机,因为我们亲眼所见它们是多么强有力.我们特别会对自己使用的第一项技术产品一见钟情.很自然地,我们很爱自己的第一辆车,但是该发生的总会发生——有一天,我们发现应该换新车了,否则就不得不花一大笔钱来维护那辆老古董.当然,不断地更新换代正是发烧一族想要的,但对于只是将车当作一种实用的交通工具的人来说,情况就并非如此了.计算机行业已经为维护超期运行的系统而付出了太大的代价——典型的例子是“千年虫”问题.
人们也趋向于钟情具有较多用户基础的成熟技术,因为这使他们觉得自己属于一个更大的文化群体.这倒未必是坏现象,但当“强势文化”开始欺凌小群体时,当对某种技术的“忠诚”阻碍新技术的发展.窒息创新时,这种“随主流”的现象就必须受到批判了.在科学史上,这样的现象并不罕见:人们竭力保护已经建立的错误观念,只是为了保护某一权力集团的既得利益不受损害.在计算机行业,这样的权力集团更为强有力,在经济上也占有有利的地位.人们也许会将伽利略看作是这种现象的典型受害者,但在当时,对伽利略犯下罪行的那些人,可是被视为最为聪明也是最受尊敬的精英人物.
技术必须不断地被重新评价,这样它们才能进步.我们必须看到并承认现行技术的缺陷,无论我们是多么地为它们所深深陶醉.被观察到的缺陷,毫无疑问,对于某些不断地释放烟雾以遮盖事实真相的人而言是一剂苦药——这样的抵抗是业内常见的“宗教战争”之源.“宗教战争”会毁了我们的专业精神!
市场炒作非常有助于市场商人提升公众对他们产品的注意力.我最近曾听一位时装业营销人士说过,一件白衬衫不过是一件白衬衫,但一件经过市场炒作的白衬衫是你想买的白衬衫.我们不应谴责那些采用市场炒作手段的人,因为想要切入一个已有某种具极高用户忠诚度的产品居统治地位的市场,这是必然的选择.但我们对技术的正确评价不应该受这些市场手段或者产品忠诚度的影响.计算机行业是最具忠诚度的行业之一.作为消费者,我们需要对不实市场宣传造成的假象多加小心.
我们应该以实用主义的目光来看待语言功能,并专注于这些功能如何作用于软件项目的生产效率.对每个语言功能都能以事实论证来支持“好”.“坏”.“容易导致问题”.
人们过于热情地采纳面向对象技术,以至于很多人开始觉得要被这种热情“烤焦了”.我本人的面向对象学习经历始于悉尼大学,当时JancHext教授给我们上关于数据抽象的荣誉课程,在课程中我们学了Simula.我对这个语言印象很深,不少其他人同样如此,包括BjarnecStroustrup和AlancKay.几年后,我开始使用ObjectcPascal来做几个大型项目,其中的一个项目使我结识了C++.于是,我充满激情地买了BjarnecStroustrup的The C++ Programming Language第一版.在阅读过程中我略感不安,因为我这个有着几年经验的面向对象实践者,对于书中的部分概念难以理解.似乎这些我熟悉的概念被某种复杂性掩盖了.在那时,C++还是非常简朴的——与当今的C++比确实如此.最初,C++比之ObjectcPascal并无实际优势可言,那时它不具备多重继承.模板等.我不想脱离实践经验地来评判语言,因为我觉得实践经验应该可以很快澄清一些模糊之处.
一个行业中的专家群体可以更具自我批判精神.医生们似乎很愿意让病人经受他们(指医生)自己不愿接受的治疗过程.许多医疗手段天生具有危险性,而寻找替代手段的研究则进展缓慢.许多医疗手段基于问题发生后的“再修补”,而非关注病人的整体生活方式并着眼于有效的先期预防.所以,毫不奇怪,有那么多人在寻求其他的医疗手段.问题在于,有太多的人不负责任地鼓吹伪科学的“替代手段”,而且其中不乏试图投机获利的骗子.但是,我们也应该看到某些替代手段背后隐藏着真理,也许它们才是正确的解决之道,可惜的是,行业中的专家群体往往本能地拒绝它们.
不幸的是,计算机行业也存在这样的问题.出于安全性考虑,为了维护某些举足轻重的大公司的利益,许多计算机专家都不愿意探索和鼓励替代手段.如果人类要继续进步,我们应该对研究投入更多,而那些实践者们则应该变得更为心胸宽广.
专家们也被各种规则束缚了手脚,而不幸的是,这些规则常常并不正确.例如,如果某一条规则具有欠缺之处,人们常常制定第二条规则来弥补修正,而非从根本上找出原因解决问题.当然,这并不意味着我们不需要监督者来制止行业中投机者哗众取宠.不劳而获的行为.
目前计算机行业中还有许多人用漏洞百出的论据来为现状辩护,这一事实说明了我们还有很长的路要走.
"为了增进人类总体的幸福,只关注应用科学是不够的.对人类本身及其命运的关注,永远应该成为所有技术努力的主要兴趣所在.为了让我们思维的创造能为全体人类谋求福利而非带来灾祸,我们还应当关注劳动力组织与产品分配这两个重大而未决的问题.当你们沉醉在图表和方程中时,不要忘了这一点."
——阿尔伯特·爱因斯坦
Posted by: hi | (141) September 27, 2007 02:05 PM
纯属个人爱好,窃以为几种语言连起来一起用是最好的了,唉……
Posted by: vczh | (140) September 21, 2007 10:27 PM
我觉得,最重要的不是C++烂.而是有很多自以为是的很烂的C++程序员.
Posted by: Anonymous | (139) September 21, 2007 05:59 PM
我用VB的,平易近人一点,编得开心一点.
例如VB:if ... then.... 连小学生都明白.
C: if(..){..};(如果现在的C语言的人死光,只有天知道是什么意思)
Posted by: 小文 | (138) September 20, 2007 08:36 AM
其实我个人觉得不一定完全是c++的问题,当然某种程度上由于c++的"易用性"(当你面对一个刚学习程序的人,当你发现你无法给他解释一个简单语句后面可能发生的事情的时候)导致很多程序员缺乏对底层的了解,但这不仅仅是c++造成的吧 :),越来越复杂的系统 @_@,迫使你依赖某些额外的东西。
C++的确有时候很讨厌,比如一大堆的封装,导致你很难理解某代码内部运行机制(某些有意无意的封装),c++带来的问题有点像交流的问题,c++的封装通常非常个人的创造,这也带来一个问题,一个不熟悉你风格或者思路的人通常很难理解。我想看c++代码的人有时候会有一定感触。哈哈,相比较而言,可能c更直接,我要实现什么功能,就直接了当,上,而不用考虑7788的东西。
这一切都是越来越复杂的系统带来的问题。当然c++由于其目的,肯定会被更方面的语言替代,而c,由于其纯粹性(接近底层),估计在很长时间内很坚挺。
Posted by: dkf | (137) September 19, 2007 05:39 PM
重剑无锋
其实我个人觉得不一定完全是c++的问题,当然某种程度上由于c++的"易用性"(当你面对一个刚学习程序的人,当你发现你无法给他解释一个简单语句后面可能发生的事情的时候)导致很多程序员缺乏对底层的了解,但这不仅仅是c++造成的吧 :),越来越复杂的系统 @_@,迫使你依赖某些额外的东西。
C++的确有时候很讨厌,比如一大堆的封装,导致你很难理解某代码内部运行机制(某些有意无意的封装),c++带来的问题有点像交流的问题,c++的封装通常非常个人的创造,这也带来一个问题,一个不熟悉你风格或者思路的人通常很难理解。我想看c++代码的人有时候会有一定感触。哈哈,相比较而言,可能c更直接,我要实现什么功能,就直接了当,上,而不用考虑7788的东西。
这一切都是越来越复杂的系统带来的问题。当然c++由于其目的,肯定会被更方面的语言替代,而c,由于其纯粹性(接近底层),估计在很长时间内很坚挺。
Posted by: dfk | (136) September 19, 2007 05:30 PM
有一个问题请教:如果要楼主设计一个比较大的商业性的应用系统,其中的中心服务器(适应于WIN和Linu,unix)用C++还是用JAVA?
有人说用JAVA,因为它便于移植(可以说不用移植)
有人说用C++,因为它性能好.
还是开发多套版本以用于不同环境?
Posted by: flying | (135) September 19, 2007 11:01 AM
楼主是搞底层的高手啊..
我觉得C和C++各有所长.
C比C++更适合搞系统级软件,但在搞商业性的应用软件时用C的话是不适合的.
不知道说得对不对,请指点..
Posted by: flying | (134) September 19, 2007 10:45 AM
哇,楼下的代码贴的好整齐!
Posted by: nothanks | (133) September 19, 2007 09:03 AM
to david:
我觉得你这个实现实际上比C++的要复杂,每个函数都多了一个参数,而且你一样需要将结构体的定义放在头文件中。
Posted by: Anonymous | (132) September 18, 2007 07:16 PM
实在要贴代码的,可以考虑加上 pre 标签。
Posted by: Cloud
| (131)
September 18, 2007 06:09 PM
晕,代码全乱了 :)
Posted by: david | (130) September 18, 2007 05:14 PM
To 楼下的:
你这个用C实现代码不会多多少吧。
而且用C实现, 头文件只暴露接口, C++反而暴露了些成员。
//------------------------------------------------------------------------------
// c-implement.h
//------------------------------------------------------------------------------
enum PURPOSE { NO_PURPOSE = 0, INIT_PURPOSE, SHUTDOWN_PURPOSE, FRAME_PURPOSE };
typedef void (FUNC_PTR)(void func_ptr, long purpose);
typedef struct _STATE STATE;
typedef strcut _STATEMANAGER STATEMANAGER;
STATEMANAGER* SM_Create();
void SM_Destroy(STATEMANAGER *);
void SM_Push(STATEMANAGER* sm, FUNC_PTR func_ptr, void* data_ptr);
BOOL SM_Pop(void* data_ptr);
void SM_Pop_All(void* data_ptr);
BOOL SM_Process(void* data_ptr);
//------------------------------------------------------------------------------
// c-implement.c
//------------------------------------------------------------------------------
#include "c-implement.h"
struct _STATE
{
FUNC_PTR func;
struct _STATE* next;
};
struct _STATEMANAGER
{
STATE * stateparent;
};
STATEMANAGER* SM_Create()
{
STATEMANAGER* sm = (STATEMANAGER*)malloc(sizeof(STATEMANAGER));
assert(sm);
sm->stateparent = NULL;
return sm;
}
void SM_Destroy(STATEMANAGER * sm)
{}
void SM_Push(STATEMANAGER* sm, FUNC_PTR func_ptr, void* data_ptr)
{}
BOOL SM_Pop(void* data_ptr)
{}
void SM_Pop_All(void* data_ptr)
{}
BOOL SM_Process(void* data_ptr)
{}
Posted by: david | (129) September 18, 2007 05:12 PM
以后不要在这里贴代码了。我就是用 MT 随便架了个 blog ,对贴代码支持的不好,也不想折腾。
另外大篇的代码贴上来,一般人都没心思看。
至于改写,没啥必要。描述好问题,用相应的语言设计并编码就成了。
Posted by: Cloud
| (128)
September 18, 2007 04:54 PM
楼下这段代码我好像在一本《DirectX角色扮演游戏》里见过类似的
Posted by: nothanks | (127) September 18, 2007 02:16 PM
请教云风大大,这段代码用C怎么改写?
enum PURPOSE
{
NO_PURPOSE = 0,
INIT_PURPOSE,
SHUTDOWN_PURPOSE,
FRAME_PURPOSE
};
typedef void (*FUNC_PTR)(void* func_ptr, long purpose);
//==========================================================================
// Defines for state manager.
//==========================================================================
class STATE_MANAGER
{
private:
struct STATE
{
FUNC_PTR func;
STATE* next;
STATE()
{
func = NULL;
next = NULL;
}
~STATE()
{
delete next;
next = NULL;
}
};
protected:
STATE* _state_parent;
public:
STATE_MANAGER();
~STATE_MANAGER();
void Push(FUNC_PTR func_ptr, void* data_ptr = NULL);
BOOL Pop(void* data_ptr = NULL);
void Pop_All(void* data_ptr = NULL);
BOOL Process(void* data_ptr = NULL);
};
//-----------------------------------------------------------------------------
// Constructor, initialize state pointer which pointer to parent state.
//-----------------------------------------------------------------------------
STATE_MANAGER::STATE_MANAGER()
{
_state_parent = NULL;
}
//-----------------------------------------------------------------------------
// Destructor, pop off all functions.
//-----------------------------------------------------------------------------
STATE_MANAGER::~STATE_MANAGER()
{
Pop_All();
}
//-----------------------------------------------------------------------------
// Push a function on to the stack.
//-----------------------------------------------------------------------------
void STATE_MANAGER::Push(FUNC_PTR func, void *data_ptr)
{
// don't push a NULL value
if(func != NULL)
{
// allocate a new state and push it on stack
STATE* state_ptr = new STATE();
state_ptr->func = func;
state_ptr->next = _state_parent;
_state_parent = state_ptr;
// call state with init purpose
state_ptr->func(data_ptr, INIT_PURPOSE);
}
}
//-----------------------------------------------------------------------------
// Pop a functoin off the stack.
//-----------------------------------------------------------------------------
BOOL STATE_MANAGER::Pop(void *data_ptr)
{
STATE* state_ptr;
// remove the head of stack (if any)
if((state_ptr = _state_parent) != NULL)
{
// first call with shutdown purpose
_state_parent->func(data_ptr, SHUTDOWN_PURPOSE);
_state_parent = state_ptr->next;
state_ptr->next = NULL;
delete state_ptr;
}
// return TRUE if more states exist, FALSE otherwise.
return (_state_parent != NULL);
}
//-----------------------------------------------------------------------------
// Pop all functions off the stack.
//-----------------------------------------------------------------------------
void STATE_MANAGER::Pop_All(void *data_ptr)
{
while(Pop(data_ptr) == TRUE)
;
}
//-----------------------------------------------------------------------------
// Process top-most function.
//-----------------------------------------------------------------------------
BOOL STATE_MANAGER::Process(void* data_ptr)
{
// return an error if no more states
if(_state_parent == NULL)
return FALSE;
// procress the top-most state
_state_parent->func(data_ptr, FRAME_PURPOSE);
return TRUE;
}
Posted by: Anonymous | (126) September 18, 2007 01:11 PM
to 11: 那么我问的就是用C实现几种类似但又不同的cache(IPv4 cache, IPv6 cache, URL cache, domain cache等等) 如何避免重复代码的问题啊。还请指教。没必要把Python扯进来吧。
至于内存,说成1万台和2万台电脑的区别确实不太合适,不过如果我们用的是廉价的PC,不用专用设备的话,每台电脑能插的内存数目终究是有限的,呵呵。
Posted by: Wesley | (125) September 17, 2007 02:49 PM
那个路人甲,说的就是git,而不是linux内核
你去看下原文就知道说的就是git
Posted by: 11 | (124) September 17, 2007 02:41 PM
to welsy
我说的是python实现逻辑部分,你那个ip cache显然应该用c实现,就和linus实现git的分布式存储不用数据库或者其他语言自带的抽象,他自己实现一个一样,,
不会多占据什么资源。而且也不该是1万台和2万台的差别,而是内存有这个差别,这是两回事
Posted by: 11 | (123) September 17, 2007 02:39 PM
to 那位11同学,
首先,消除重复代码对性能没影响。但对代码的可读性和可维护性有影响。我想,大家都认同,基本没有重复的代码才是好代码吧。
其次,不用Python的理由很多,其中一个理由是Python是一种dynamically typed language,它的实现是每个value object都会有一个tag用来表示它的类型,这个tag到底占多少空间我没研究过(可能是implementation/machine dependent的),不过至少一个byte,如果考虑到alignment的问题,在32位的机器上多半是占32bit也就是4个byte。这意味同样实现存IP address的cache,用C实现的话一个cache entry只占32 bit也就是4个byte(存IPv4的address够了, IPv6多一些),而用Python实现的话同样用一个整数来表示IP address,大概就要8个byte了,因为需要额外空间存这个value的type tag。也就是说同样数目的内存,用Python实现的话cache里面存的东西要少一半。如果每台服务器的内存是固定的,那么假设果我们本来用1万台服务器就能满足需要,改用Python实现后就需要2万台了,这增加的不仅仅是硬件成本还有电费和管理成本。做这种以service方式部署的产品是不能不考虑运营成本的。
Posted by: Wesley | (122) September 17, 2007 02:30 PM
偶以为,之所以有这个问题,潜台词应该是:Linus老大用C写出了Linux核,而不是什么Git。说C++好的,去用C++搞一个Linux++,估计就不会这么底气不足了。
反过来,是不是说一定要写出了某个OS、kernel才能算得上好语言呢?
语言都是用来抽象的。
Posted by: 路人甲 | (121) September 16, 2007 08:57 PM
补充一下,linus不用数据库而自己写,就是linus作的工作用c没用c++的原因所在,那个走来要用c++的人根本没看到问题所在,c++要完成linus作的那部分工作无用武之地。那部分不需要c++的抽象,c++的抽象可以发挥作用的地方用python代替了
Posted by: 11 | (120) September 16, 2007 11:35 AM
那个wesley,你的消除重复代码真的对性能很有影响?
我觉得这种算法的东西,用python来实现和用c++实现效率基本没什么区别,甚至可能实现的更好,因为python有很多非常高级的抽象和优化在这些算法上,你用c++不一定能设计的那么巧妙,而linus所作的更多的其实是在硬件I/O层上的模型对象设计,换句话说,他那个git其实是linus自己实现了个数据库,而逻辑部分用python实现的。
你那个c++用模板解决重复代码我想如果代码足够大,那么用c抽象一个层出来绝对还是值得的。如果代码量很小,其实无所谓,并不会有什么维护难度
linus用c而不用现成的数据库完成这些个对象的merge,tag什么的,我想他不想用数据库那么大的对象,资源限制了。git可能有几百万个文件,补丁树什么的,如果用数据库,要用的资源就是海量的,而大多功能其实用数据库那臃肿的模型没必要。linus就是用自己来实现这些需要的对象的内存化操作管理,而且需要的对象可以随时定制和添加修改,。
用c++就是大炮打蚊子,深入不到那个层次,用C++也用不到抽象那个层次,换句话说你还是用的c++的c部分,真要抽象的地方,git用python去作了,python对c是天然融合的,可以做到更可读更简介,更高效,无论是逻辑还是硬件,你用C++都做不到这个层次
Posted by: 11 | (119) September 16, 2007 11:32 AM
前面的讨论完全可以编个现代寓言留给子孙后代读了。寓言的名字就叫《瞎子摸动物--新编瞎子摸象》吧。
从前啊,有这样一群瞎子,他们没见过动物,有一天,为了搞清动物是什么,他们要去摸摸动物。他们先去摸大象,在把象腿摸过一遍之后,不屑一顾地说:“原来大象就是根柱子。”然后,他们再去摸刺猬,在碰过刺猬的刺之后,自以为是地说:“原来刺猬就是根针。”接着,他们开始比较起刺猬和大象来。一个瞎子站起来说:“有了针状刺猬,我对柱子状大象感到很恶心,因为针比柱子更锐利,刺猬一定会杀死大象。我预言,大象会远去,刺猬会回归,并将以其无比的锐利统治世界!什么?不对?你们好好想想吧,也许你们还没思考到我这一层。”底下七嘴八舌有几个瞎子在附和,甚至开始欢呼他们找到了真理。小朋友们,谁能告诉我这个寓言故事说明了什么道理呀?
刺猬当然不是针,大象也不是柱子,刺猬更没有统治世界。千百年过去了,刺猬、大象和其他万物一起生活在共同的世界里,当然也包括那些瞎子。希望我们的子孙后代每个人都有机会受到这则寓言的教育,把思维瞎子的数量降到最低。
Posted by: Anonymous | (118) September 16, 2007 08:29 AM
程序员们可怜到了以讨论两门语言的优劣来找乐子了么??有这个时间,哄哄刚出生的baby多好!
Posted by: wodiw | (117) September 15, 2007 11:22 PM
在我看来,所谓设计,就是创造概念的过程,如何实现,就是Linus所谓的BS。设计没有对错,只有好坏。问题在于,人的智力有限,再牛的人,也要依赖更基础的抽象工作。牛人可以同时顾及的东西更多一些而已。但正如过往的研究指出的,人同时能够track的维度实在有限,不过就是5~7个。OK,就算Linus是天才,但我不觉得他会比Andrew Koenig之辈高出一个数量级。而从Koenig的文章中看出,每每代码要track 3个以上的维度时,就要说怀疑其中是不是完全正确了。
我宁愿相信这次不过Linus受人刺激之下的大嘴巴。从过去的历史来看,Linus也确实嘴巴比较大。
在从技术上来说,说Linus是软件工程方面的天才更让人信服一些。De Raadt讽刺linux的话也不是空穴来风。要是有人说Linus是操作系统大师,我宁愿相信Rob Pike的话,现在的操作系统不思进取。
Posted by: wingfiring
| (116)
September 15, 2007 02:43 PM
没人提Lua啊?个人觉得C+Lua非常好,C实现物理层,Lua实现逻辑层。
Posted by: churunmin@gmail.com | (115) September 15, 2007 01:30 AM
把c 和 c++放到我面前让我选,我会选c++,一定要我用c,也没有问题,但我会找一些现成的库来用,比如bstring之类的。
Posted by: 混口饭吃 | (114) September 15, 2007 12:49 AM
设计第一,语言第二。
C/C++没什么需要比较的。
太重视语言,人就抽象了。
多做点实事,比谈理论更好。
Posted by: Initial_X | (113) September 14, 2007 03:35 PM
阿弥陀佛 vs 哈里路亚
Posted by: PX | (112) September 14, 2007 03:31 PM
to Wesley,
最近我们新项目内部测试,忙晕了。
我想了一下,你的这个问题不太好回答。我用 C 做设计时间不太久,经验积累的没有当初用 C++ 时的多。
先说几句问题之外的话,我的感觉是 C 的解决方案往往是直指问题的。不主张再未了解问题全部之前做半拉子事。也就是我们在 C++ 做设计时常用的那种“抽象”。C++ 做设计时,很容易因为不了解最终要做什么,就先抽象出来把其他的部分完成。
抽象本身也没有错,错的是抽象层次过多。什么叫过多?超过三层就是多了。我认为抽象层次过深造成的危害是略大于重复代码的危害的。(当然两着都是很严重的危害,都需要避免)
话说回来,这个问题本身在你看来不是设计问题,是实现问题。那我们再来考虑这个实际问题。
我对具体问题还不是很了解,也不可能做太多具体分析,只能给出我在我们项目中遇到的类似问题的解决方案。强调一下,我的一个观点是:具体问题即使有细微的不同,解决方案可能差别很大。企图找到通用方案很可能陷入过度设计的泥沼。
我们的系统首先是按模块划分的,这个模块的具体实现可以是系统提供的 so ,也可以是自实现的二进制数据块。在这里我们并不用类似 C++ 的继承手段来实现模板方法。而是用模块 id 来做耦合。
类似 cache基类 的东西会是一个独立模块(对整个系统来说以二进制形式给出,而不是源文件),不同的策略以分离的不同的独立模块存在。
cache 基类需要不同的策略的时候,加载不同的策略模块。它可以通过 id 或 string 从配置里完成耦合。这个过程可以是初始化时做,也可以是运行时做。
我提到的这个设计方案,用在我们的资源管理器上。曾经在 blog 上写过: http://blog.codingnow.com/2007/05/mutilthread_preload.html 。它可以完成从不同介质(内存,本地文件系统,打包文件系统,网络)加载资源;也可以采取单线程和多线程预测加载多种方案。
不过上次没写具体设计。实际上,我提供了一个很简洁的函数接口给最终程序员用,再提供了一套独立接口给扩展策略的程序员用。两者分离可以使每个不同职责的人可以看到相对干净的接口。
Posted by: Cloud
| (111)
September 14, 2007 03:15 PM
晕,如果说设计只有抽象,那么对设计的理解也太过狭隘。
Posted by: rockcarry | (110) September 14, 2007 02:36 PM
Linus说话自相矛盾。
诚然,除了设计,其他的都是BS.问题在于,Linus在设计什么?难道不是在设计抽象?不依赖抽象进行设计?文件不是抽象?字符串不是抽象?又要强调设计,又要否定抽象,我不知道想表达些什么。
我唯一看出来的就是,不过就是说别人的抽象不如他的好而已。
Posted by: wingfiring
| (109)
September 14, 2007 02:25 PM
to Atry: 张口教训人之前先搞清楚别人的问题好不好。我没有问接口问题,没有问设计问题(我说了我设计时只考虑整体构架,考虑选择用何种数据结构和算法,根本不考虑什么语言的)。我问的是实现问题,在我说的那种情况下用C实现如何避免重复代码的问题。
在这里模块就是cache component。别扯什么模块化,也别瞎猜测头文件里放了啥。我前面说得很清楚,这个模块我当时还是决定用C实现的,如果你好奇的话,可以告诉你,头文件里只暴露了几个函数作为interface。没有暴露任何数据结构,而是把结构指针cast成不透明的void* handle传递出来的。大致这个样子(简化过的,以利于说明问题。)
当然,如果用C++写的话这个接口中的void* cache_handle就相当于this指针了。接口大致上是一个cache类,然后提供update/lookup两个方法。(实际的实现会有一些别的方法,比如持久化什么的,不过这里update/lookup足以说明问题。)
我的问题是,因为`cache_lookup()`和`cache_update()`的内部实现其实是要分别查询好几个cache(比如先后调用`domain_cache_lookup()`, `url_cache_lookup()`等等), 哪个cache命中就返回哪个结果,那么用C如果不用宏的话如何避免让`domain_cache_lookup()`和`url_cache_lookup()`有大段重复代码。这2个函数操作的是类似但不同的`domain_cache`和`url_cache`的数据结构(细节不用我说了吧)。
至于为什么要用不同的结构,前面已经说了,这个应用对性能和内存利用率的要求很高。事实上可以告诉你这个`cache component`是一个web reputation service的一部分,这个web reputation service需要储存大概几十亿或者更多的URL的分类和声誉信息,部署在多个数据中心,为Cisco在全球销售的多款路由器产品提供backend的URL/IP的分类和声誉服务。
我只是觉得用C++的话在这种情况下很容易避免重复代码。用模板可以让操纵不同缓存数据结构的代码只写一遍,让编译器来做copy-paste的工作。用继承也可以实现类似的目的。用template method可以把不同cache的lookup/update逻辑中公共的部分只写一遍,不同的部分分开。但用C怎么办呢,有好的做法吗,我想知道你们推荐的做法。仅此而已。
Posted by: Wesley | (108) September 14, 2007 02:10 PM
与楼主工作在同一领域,一直觉得楼主比较牛,看了这篇文章才发现楼主还在讨论这个问题,失望。。。
Posted by: nette | (107) September 14, 2007 12:54 PM
我算不上精通C++,但也是周围的人中最熟的.
觉得C++ 设计过度了.
现在尽量只用C++中最基本的功能.
这些功能,用C是足够了.
老实说,在这里为C++鼓吹的人,对C++的了解不一定有多深.
Posted by: mike | (106) September 14, 2007 12:07 PM
用Java吧。
Posted by: Anonymous | (105) September 14, 2007 11:00 AM
我用脚趾头猜测了一下,你写的代码在头文件用 C++ 暴露了很多不该暴露的东西,然后你开始抱怨 C 没那么容易暴露这些东西。
Posted by: Atry | (104) September 14, 2007 10:07 AM
蠢人,蠢人。你知不知道,C++的常规继承和模板对于模块化只有负面影响,这两个特性只对于减少代码有好处,对于设计是有害的。
我猜你写代码的时候,根本不知道什么叫接口。
说句讨打的话,我鄙视设计模式。主要还不是鄙视设计模式本身,而是鄙视那些愚蠢的程序员用愚蠢的方式使用设计模式。
Posted by: Atry | (103) September 14, 2007 10:04 AM
无意比较C和C++的优劣,只想指出一点,抽象未必是为了复用。更多时候仅仅是为了消除重复代码,在不损失效率的前提下提高代码的可维护性。
举个例子。我曾经做过一个性能和内存利用率要求都很高的缓存组件。这个缓存组件可以存放URL(为了节省内存,只存放URL的fingerprint,比如MD5,一个128bit的整数),以及domain name(这个要求存完整domain name),以及URL + IP address一起缓存(要多存一个32bit或者128bit的整数,取决于是IPv4还是IPv6)。
做设计的时候,我根本没有考虑用C还是C++实现的问题。我只是针对需要储存的不同类型元素,选择了几种数据结构(几种自平衡树,几种不同的internal/external chaining hash table,还有几种采用不同空间压缩策略的trie结构)做了一些分析和benchmark。其他的构架上的design decision也都是和语言不相关的。
实现的时候,为了避免浪费内存,看来只能把这3类情况分开处理,做了3个不同的缓存实现。但是,其实这3个实现很多代码和逻辑都一样的。当然cache entry的结构定义、类型,以及查找/更新cache entry的代码中会有一些细微的不同。
如果用C++的话,可以让3个cache都继承自同一个cache基类,把相同的代码放到基类的实现里面去,也可以把cache_lookup和cache_update所操作的结构定义成template,然后把那些不同的操作特化一下。
但C里面就没有什么好用的抽象机制来消除重复代码了。用宏?可读性和可维护性比较差。copy-paste然后做些细微改变?可维护性很差。
从设计的层面看,有个叫template-method的pattern可以解决这个问题。但要实现的时候就会发现这个pattern需要语言提供继承语义,否则很难实现。当然,我们可以用C来模拟一个继承的实现。但既然如此为何不用C++呢。
其实,这个缓存组件,我后来还是选择了用C写。不过我想这类“为了消除重复代码而需要抽象”的情形应该是编程时会经常遇到的。这里的抽象和复用无关。我不太清楚云风或者其他喜欢用C的朋友都是如何解决的呢?
Posted by: Wesley | (102) September 14, 2007 06:10 AM
关于编程语言的争论就跟关于宗教信仰的争论一样没完没了,并且经常是非理性的,即使偶而出现比较理性的论点,也往往难被其他人接受。
其实嘛,人的胃口和喜好是如此的不同,即使再糟糕的语言也还是会被一部分人喜欢,再好的语言也可能会令另一部分人讨厌。况且语言也确实存在不同的最佳应用领域,抛开应用领域比较语言的优劣肯定会衍生为没完没了没有结论的争论。
在一个项目上,云风从C++转到C似乎不能作为C更优于C++的佐证。我想在用C重构C++程序时,在设计方面某些地方也许还得归功于前面使用过C++。我也用C重构过一个C++程序,结果也很令我满意,感觉用C重构后的比原先C++写的好。但我觉得先用C++写对后面重构帮助很大。要是我一开始就用C写,肯定会写得一塌糊涂;-)。不知道云风是否也觉得先前用C++开发的游戏引擎对后面用C重写有比较大的帮助。
另外,我觉得,C似乎对人编程的功底要求比较高,功底不够,写出来的程序肯定既难看又难维护。C++在这方面要好一些。如果一人C功底好又不怕多写代码的麻烦,选C是情有可原的,因为C给他们的束缚更少(就是云风这类人;-))。如果要快速开发一个大型应用程序且能满足效率要求,C++应该是比C和动态脚本语言更好的选择。
Posted by: phoolimin | (101) September 13, 2007 10:39 PM
C++社区努力实现了STL,BOOST之类的库,很大程度上就是为了为了代码复用,尽管你认为这些努力有时候看起来不是那么的成功. 问题是,对比之下的C, 这方面是更加的失败.
我认为之所以C++被Linux这样的内核开发人员所唾弃,有一个重要原因是,他们如果用C++,等于是被强迫适应另一帮行家所实现的产品,而身份内核开发人员习惯控制一切,了解一切,怎么可能接受一个不清楚的std::string实现!! C和C++尽管他们有很大的重合,但来到底层开发这样一个特殊的领域,还是有很大分野的.
而且在底层开发讲求的是效率,稳定性等诉求,C++能给予的帮助真的并不多.在这个等级对C/C++进行取舍是有其特殊背景的,并不能把结论随意推广. 你可以证明C实现了伟大的OS,但C++同样也可以证明它实现了规模更大的Vista和无数的软件. 他们争吵的这个背景相当特殊,对C来说差不多是最后的重要领地了.
补充一句,C++最大的问题是它太容易被不恰当地使用了.
Posted by: 果子 | (100) September 13, 2007 10:20 PM
不少人为这个争论找台阶下,说 C 和 C++ 各有适用的领域。而我不这样看。
这几句话本来不想说出来,因为这太不厚道了。对这个无聊的话题,保持沉默才是最好的态度。
刚才看见下面 fastzhao 说的 symbian os 的例子,感觉真是太有趣了。本来我想拿这个例子来败坏 C++ 名声的 :D 看来什么样例从不同的人放在不同的角度来说,就有不同的效果。因为这件事的有趣,忍不住还是回复了。
我对 Linus 的话产生共鸣,正是因为他说出了我心底的话。Linus 大大在整个争吵过程中,从来没放什么适用领域这种话。他只是说,你若想用 C++ 做的版本控制系统,那就用去。世界上有这种选择,没必要来用我的 Git 。
操作系统也可以用 C++ 写,symbian os 就是一例。我 2000 年接触这个东西,那个时候叫 epoc 。曾经有两个月就是靠这玩意骗钱吃饭。
后来用上了仰慕已久的 symbian 手机,那叫一个痛苦: (见 http://blog.codingnow.com/2006/07/eoeueoiaee.html )。还是现在换成了 palm 才觉得玩手机也很有乐趣。
游戏引擎也是这么回事,其实用 C++ 做这个工作的人可能更多。并非适用面决定了该用什么语言开发。
我始终认为 C 和 C++ 适用领域是高度重合的。
谈到效率,论用程序完成一件事情的理论效率极限,C 是比不上 C++ 的。原因很简单:C 是 C++ 的一个子集,多出来的那些语言特性理论上可以提升代码效率。
追根到底,软件的质量(可维护性,可扩展性,效率)这些是程序员本身的素质决定的,而不是他们用的语言。
强迫一个热爱 C++ 的程序员换 C 去做软件,软件质量不可能上升。
但是,当他发自内心的回到 C 后,情况就不一样了。
为什么会发自内心的做这件事情?如果能写清楚并用文字说服其他 C++ 程序员。那么早有牛人写出来了,轮不到我这种小字辈。
Posted by: Cloud
| (99)
September 13, 2007 02:45 PM
其实语言之争没有任何意义,语言只是个信仰的问题。但是我还是要为c++说几句公道话。
普遍都认为c++不适合做系统开发,那么请问symbian os是用什么开发的。 nokia和索爱的智能手机大部分都采用symbian os。
symbian os就是将c++用于系统开发最好的例子。其中用的什么瘦模板,分段构造c中有吗?所以说c++不是你想像的那么可怕和阴险。而是看你怎么用它了,c++的设计思想就是提供好多特性,让你选择。 而c呢? 你恐怕没得选择吧!
Posted by: fastzhao | (98) September 13, 2007 02:09 PM
前面那个叫着用Ada的,牛叉是牛叉,就是在国内难找工作.
Posted by: lk | (97) September 13, 2007 10:17 AM
一帮疯子
C和C++和Java都有自己适合的用武之地,谁也抢不了谁的地盘.
我现在作的项目就用C++,改用C?让我去死吧.
改用Java?死得更快.
只要想清楚这一点,利用语言,而不是被语言俘虏,那也就没有什么好争得了.
Linux就其项目而言说C++不合适也算可以理解,如果推而广之来骂C++,那其心真可诛也.
Posted by: rainwolf911 | (96) September 13, 2007 10:05 AM
的确是信仰问题。
Posted by: 安德尔斯 | (95) September 13, 2007 07:10 AM
从这个看起,把孟岩和刘江的blog看过后,竟然又燃起了我从前对C++技术学习的热情,我爱C但更爱C++。
Posted by: 安德尔斯 | (94) September 13, 2007 07:09 AM
用vc的话 ms为C提供了__inline扩展
Posted by: x | (93) September 12, 2007 11:57 PM
都用ADA2005吧,比C、C++都牛叉!
Posted by: simgenes | (92) September 12, 2007 10:27 PM
为什么会有这么多语言?每种语言自然有它擅长的一面,世界上没有十全十美的东西,要不然也不会冒出这么多的。我们还是要用最简单的方法去实现最复杂的事情,当然也不是要去学会所有工具,只要精通一样就可以了,谁有三头六臂呢?
Posted by: yinRed | (91) September 12, 2007 07:53 PM