C语言是一种简单优美的语言,但是它也有很多问题。C++试图解决C语言的某些问题,并为此付出了努力。这种努力值得钦佩,但是其结果不如人意。由于对C++的大量投资,人们往往对C++在表面上解决了C问题的假想夸大其辞,而对于C++继承C的问题过分忽视。
C++对C的兼容性是一个错误。这不是因为C++继承了C的错误,而是C++的继承导致了C++特有的错误。C的很多策略在其简单的环境下是可控的。当C++拥有比C高一个数量级的复杂度的时候,这些策略就成为了致命的问题。所以,C++对C的继承和兼容并不意味着C++“至少比C好”。C++在(表面上)解决一个问题的时候引入了(实质上)更多的问题。
分离编译(separate compilation)是C语言编译的基本策略。在此基础上C借助make等工具实现了增量构建(incremental build)。分离编译中,不同编译单元(unit of translation)中的信息共享通过头文件来完成;最后的信息汇总通过连接器完成。分离编译——特别是在借助于头文件这种低级语言处理方式,并且最终必须通过静态连接生成可执行文件的情况下——有它自身的局限性,但是在一个相对简洁的语言中,还是可以维护的。
在C的分离编译中,一个编译单元可以引用另一个编译单元中的实现。后者对其包含的实现(比如函数,全局变量)输出symbol。前者引用symbol。二者间symbol的一致性通过头文件保证。连接器通过symbol的对应来进行连接。如果两个不同的编译单元输出了同样的symbol,连接器会报错。如果一个编译单元引用的symbol没有被找到,编译器或者连接器会报错。这样的结果是,放在头文件中的实现往往会导致连接时的重复symbol定义错误。所以,部分的是头文件的设计初衷,部分的是这种简单的连接策略的结果,头文件在C语言中半强制性的地位是包含类型声明,而不能包含类型的实现。C语言的简单机制保证了绝大多数编译单元间不一致造成的错误能在编译和连接阶段被发现,同时提倡了头文件功能的统一。
在C语言没有提供保护的广大领域中,容器的类型安全是最为C++的鼓吹者津津乐道的。C++解决容器类型安全的方法不是为语言处理加入容器类型安全的特性,而是发明了模板这个meta-feature。通过模板,C++的设计者感到满足——因为面对问题,C++没有头疼医头、脚疼医脚,不是简单的ad-hoc解决,而是引入了优美的新的基本概念,从逻辑基础上一举解决这个问题,还得到了编译器图灵完备的语言。
但是有些东西超出了C++设计者的预料。由于模板在编译时才能具体展开,所以模板的实现必须放在头文件中。使用模板的每个编译单元都必须包含实现这些模板的头文件。模板的问题在于,引用模板就是实现模板。所以,当单元A和单元B同时引用std::vector<a_class>的时候,它们不是在引用同一个类型,而是各自实现了一个类型。这样的结果是C++的连接器不能像C连接器那样,简单地对不同的编译单元实现同一个symbol的情况报错,C++的连接器必须决定保留某个编译单元的实现而抛弃其它单元的实现(严格的说并非完全抛弃,如果不同的编译单元使用了某个模板类不同的成员函数,这些不同的成员函数的实现会被合并)。
当不同的编译单元引用了同名的模板附带同名的类型参数,但是这些模板或者类型参数在不同的单元中的定义不同(比如不同的编译单元使用了不同的编译宏、或者引用了含有同样名字模板的不同头文件),结果是灾难性的。任何一个单元的编译都是成功的。连接呢?当你遇到一个具体的编译器之前,你绝对不知道;当在一个莫名其妙的错误上花费无数时间之后,你也许会猜到几分。在Visual C++上对不同的编译单元打开或者关闭_HAS_ITERATOR_DEBUGGING,你会发现,连接默默的获得了成功,但是(幸运的话)程序会马上崩溃。
是的,C也会有类似的问题。但是C的问题仅仅局限在struct定义,而不会延伸到函数实现。大多数由于包含不同struct实现而造成的分离编译问题实际上都能在编译时发现。因为struct本身具有接口的性质,而接口的错误往往会造成同一个编译单元内部信息的不一致,从而在编译时被发现。但是模板不一致造成的问题往往属于实现问题,在编译时被隐藏,在连接时又被考虑不周的连接器忽视。C++在C的基础上引入了新概念,但是新概念并不是基于更智能的编译策略,而是一种简单的即时(on-the-fly)类型生成。这种代入风格[1]的扩展把C语言中无关痛痒的瑕疵发展成了致命的缺陷。C++的初衷是增强C的静态类型系统,把更多的错误扼杀在运行时之前。但是由于失去控制的复杂度,最终导致一些在C语言中本已解决的问题退化为运行时的问题。
注:[1]关于代入风格的讨论会随后整理。代入风格即处理输入信息的时候局部分离处理。和委托风格对应(即处理输入信息时全局统一处理)。
2009/06/01 1:22 下午 |
单纯从C和C++的最终用户角度来说,我更喜欢用C++,我不喜欢C的字符串内存之类的操作,很麻烦又容易出错。你描述的C++编译连接的问题都在可控的范围内。至于副作用,C++ developer要自己长知识和技能,自己来把握。作者本人不就很了解这些吗?
另外,语言复杂没什么不好,最起码给喜欢逞能的人一个机会来显示自己有学习能力。如果语言都像Java那样,真的很无趣。如果不自杀,我就不编程了。
历史发展到目前为止,人类还没有丧失对自己创造的东西的控制。对核弹如此,C++就更不在话下了。
2009/06/02 7:08 上午 |
插嘴,和Zy同学讨论:
俺倾向于同一TopLanguage大拿莫华枫同学的名言:C++是一种功能分布不均匀的语言,有些非必要功能大量存在而另一些关键功能则不存在,于是这导致了无数大拿试图通过重载之类的牛X语法提供一个workaround,而这必然地导致低效率的实现和无数的tricky code。俺不喜欢Java也是出于相同的理由,比如Java中被无数面试大拿津津乐道的internal class和static internal class,完全是为了照顾OO原教旨主义表现力不足的问题而生造出来的概念。
至于语言复杂好不好,俺倾向于持宽容观点,怎么说俺现在对编程语言原教旨主义的容忍度比以前高得多了。至于C的字符串问题,如果不介意一些丑陋的语法的话,GLIB的g_string_*基本上还是令人满意的。但是这和C++带来的副作用是两个问题。副作用的知识好不好?俺倾向于持悲观观点。Workaround毕竟是workaround,等到正式的fix出来之后就多半倾向于废除掉了,而且更糟糕的是很多workaround与其说是workaround还不如说是show off。至少对俺来说,与其花大量的时间钻研如何写一个Boost版本的foreach,还不如老老实实写for更好一些。
最后说一句,虽然人类没有丧失对自己创造东西的控制,不过能够控制也就意味着必须承担责任,如果可以少担一点的话,俺还能早点回家多花点时间修建一下自己的草坪,何乐而不为呢?
2009/06/03 1:42 下午 |
莫华枫同学的上面让我感觉到C++好像是一块饼,上面洒的芝麻不均均令他不满。
批评C++的人,很多是出于有语言洁癖的心理,或者思想洁癖。(我不应该对别人的心理揣测太多,这样不厚道)
C++中有很多东西是发现的,而不是设计者故意设计的。STL,Template Metaprogramming都不是设计者预先料想到的。所以Bjarne给他的书起名为“C++的设计与演化 (The Design and Evolution of C++)”,我认为这是它迷人的地方,它能够进化。你不能把这些称为workaround或show off。
工程理念问题很难争论,最后大家都是鸡同鸭讲。首先是立场大家不相同,真正关心的东西也不同。
2009/06/03 1:51 下午 |
华枫同学的话让我感觉到C++好像是一块饼,上面洒的芝麻不均均令他不满。
批评C++的人,很多是出于有语言洁癖的心理,或者思想洁癖。(我不应该对别人的心理揣测太多,这样不厚道)
C++中有很多东西是发现的,而不是设计者故意设计的。STL,Template Metaprogramming都不是设计者预先料想到的。所以Bjarne给他的书起名为“C++的设计与演化 (The Design and Evolution of C++)”,我认为这是它迷人的地方,它能够进化。你不能把这些称为workaround或show off。
工程理念问题很难争论,最后大家都是鸡同鸭讲。首先是立场大家不相同,真正关心的东西也不同。
2009/06/09 10:24 上午 |
>>>>>>
工程理念问题很难争论,最后大家都是鸡同鸭讲。首先是立场大家不相同,真正关心的东西也不同。
<<<<<>>>>>>
C++中有很多东西是发现的,而不是设计者故意设计的。
<<<<<<<
这根棒子zy没打到点子上。实际上这种例子在别的语言里也同样有。比如Java 1.3引入的反射,JCP的官僚们只是当了一个赶场的小东西,却在Spring手里狠狠地打翻了不可一世的EJB;第一个把C#的attribute用到极致的东西其实是NTest,而不是MS自己的.NET framework。语言元素的再发现和创造性运用并不是C++独有的,而且作为一个四年左右C++经验的coder,俺也从来没有怀疑过C++存在进化,但是请注意:进化本身是客观存在的,无所谓好坏,但是拿这个进化做什么用则是社区和文化的责任。
俺和冯家老哥对C++走向的观点一直有一个差异:冯家老哥倾向于认为C++的问题在于它的语言设计就是烂,俺倾向于语言无所谓好坏,而C++是被缺乏关注实际功能社区风气拖累了——没错STL有用,但是它还远远不够。而对俺——只是对俺个人——而言,与其把时间花在怎么研究BOOST_FOREACH上,好好研究一下GTK+或者Dbus之类的东西要更有价值一些。
2009/06/10 1:32 上午 |
更正:NTest是NUnit的笔误。
2009/06/11 12:32 下午 |
to cppof286
Java的反射和.Net的attribute的机制和C++的模板是不一样的,因为要给Java或.Net加入metadata,而C++的模板不是这样。对发现来说,在Java和.Net中更简单些。因为这是更惯常的手法:多增加数据或从外部输入数据,来让程序有更大的灵活性。对虚拟机这个程序来说,metadata就是这个作用。
把时间花在什么内容上,是自己的偏好。大家都希望把时间花在最有效用或最本质的内容上。不过,你倾向好好研究GTK;他还倾向研究二维绘制,反走样呢;他他倾向研究计算几何和贝萨尔曲线呢;他他他倾向研究微分几何呢;他他他他倾向研究复变函数呢。
我没说C++坏话,并不意味着我认为大家每天都要学C++。很多人批评别人关心语言,有暗指这个人除了语言什么也不懂的意味,即暗指这个人层次太低,需要往高处带。CSDN上有很多此类言论(说到这里,没有针对cppof286的意思。水流至此,我顺水推舟了)。
其实,工具和内容是互换的。你今天认为的工具,明天就是你工作的内容,今天的内容或领域知识,明天就是工具。我大约记得好像是Jeffrey Richter在一本书中说,他对语言的作用报不可知的态度。因为比较难估计工具的效用。