Archive for the ‘C++’ Category

C++恶性录——之二

2009/06/10

C++中有很多东西是发现的,而不是设计者故意设计的。STL,Template Metaprogramming都不是设计者预先料想到的。所以Bjarne给他的书起名为“C++的设计与演化 (The Design and Evolution of C++)”,我认为这是它迷人的地方,它能够进化。你不能把这些称为workaround或show off

——樊志岩对《C++恶性录——之一》的评论

It’s risky to design minilanguages that are only accidentally Turing-complete. If you do this the odds are good that, sometime in the future, some clever fellow is going to think he needs to press your language into doing loops and conditionals for him. Because these are only available in an obfuscated way, he’ll produce obfuscated code. The results may be serviceable in the short term, but are likely to be a nightmare for those who come after him.

——《The Art of UNIX Programming》


认为C++“迷人”的同学中,有一部分是因为从C++里面发现了那么多意想不到的东西。意想不到的东西给人悲伤、震惊、或者乐趣。但是带来惊喜和乐趣的意想不到并不一定代表好的工程设计。

从C++里能“发现”这么多东西,是不是说明了C++的设计是经过深思熟虑、抑或就是其设计者灵感迸发、(相比那些后来从中“发现”的东西不那么多的语言的设计者更)才华横溢的体现呢?看来有一部分人是这么认为的。但是崇拜经常来自于知识的缺乏。如果你了解了“图灵完备”的概念,然后把template考虑成一种compile-time minilanguage,再把两个概念放到一起,就会把“迷人”的外衣剥落。只要你让一种语言达到compile-time图灵完备,不从中“发现”那么多东西倒是奇怪的了。达到图灵完备是任何一个语言设计者都轻而易举能掌握的技巧。事实上,正如《The Art of UNIX Programming》所告诫的,了解图灵完备概念的意义在于避免无意中触发它而不是不必要的实现它。

那么,抛开“发现”这么多东西的原因,“发现”的这些东西是对工程有实际作用的吗?“[S]erviceable in the short term, … nightmare for those who come after …” C++里面发现的东西,可读性都很差,如果出了编译错误更是没法收拾。例如,C++曾经用template实现了一个LL(k) parser generator,很奇妙么?但是这个东西产生的parser对unpredictable syntax的处理是silent failure。所以这个东西只能是孤芳自赏的玩具!

C++ template是为了静态类型安全设计的。但是当一个在高级语言里可以用smoke test很快发现的错误在C++里变成一个晦涩的编译错误的时候,只能说C++的设计者失去了更广的视角。静态安全在一定限度内是有益的,不知道在什么地方停步是C++设计者的问题。不舍得付出任何代价,用template specialization这种补丁来达到效率提升从而意外的引入“图灵完备”,最终让C++失去的东西更多。

C++恶性录——之一

2009/06/01

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]关于代入风格的讨论会随后整理。代入风格即处理输入信息的时候局部分离处理。和委托风格对应(即处理输入信息时全局统一处理)。