- 论坛徽章:
- 0
|
本帖最后由 狗蛋 于 2011-11-18 12:34 编辑
回复 13# bruceteen
这就是所谓淫者见淫。
比如这个:
Linux之父炮轰C++:糟糕程序员的垃圾语言
http://www.google.com.hk/search? ... ;btnG=Google+Search
不知你们看了,反应如何。
这个争论发生时,正是我对C++的痴迷期:
不仅早在03年或04初,我就买了侯捷的STL源码剖析,甚至去啃当时还非常激进、号称只有未来的编译器才能支持的loki库(因为这个库使用了很多“疯狂”的模板特性,这种特性在当时没有任何编译器可以支持——换句话说,这是一个先进到无法编译的模板库)。
而且,我还参考业界有名的几个经典实现,针对实际需要改进/特化了STL库的内存池,而且这个池一直跑在一个工控系统上(做过仪表的应该知道这种系统对稳定性有怎样的要求);同时,部分修改/特化了STL库的hash_map,也用在那个系统中。
甚至,07年我还用模板+继承+宏,完成了一个POD结构体类型自动打包成XML的库。这个库就俩接口: convert2XML(const T&)和conver2POD(const char * , T&),用起来和c函数调用一模一样,但却可以根据传入的POD类型自动选择合适的转换策略。
至于面向对象,早在97年已经对继承多态什么的弄的滚瓜烂熟,恨只恨当时学校机房竟然没有C++编译器,不过这不耽误我在头脑里思考自己做过的程序如果改用面向对象的话,该怎么设计。
以致于后来(05年),华为有个老想着给我穿小鞋的“高手”领导,说某个开源的unittest系统的面向对象设计非常高明,他们当时十几个人都是各部门抽出来的高手,研究了很久都没看懂;吹完了忽然突发奇想交给我个任务:写个文档,下次开会给大家讲讲,让大家都懂。
我花了一个晚上,写了个深入浅出的ppt,做到了。而且还总结了这个系统的设计思路,指出了这个系统不尽人意之处。
甚至,当时一个牛人,好像是周伟明还是谁,曾在自己博客上举例说用模板会导致代码膨胀,比如stack(T)会出现多份代码云云;我回帖反驳说你用stack(size_t)也一样,大小是必须指定的,区别只是代码写在哪里而已,和代码膨胀有什么关系?弄得他很是下不来台。
就是在这个背景下,这桩子事出现了: Linux之父炮轰C++:糟糕程序员的垃圾语言
这是当年的原帖:
http://blog.csdn.net/turingbook/article/details/1775488
我敢肯定一点,看到这个,换了本版这几天闹腾的那些人,肯定是立刻跳出来参与c教徒和c++教徒的“圣战”;把这样一个本来就带着情绪带着火药味夹杂着bullshit的学术争论,彻彻底底拖到泼妇骂街的低级水平上。
——那啥,c教的红衣大主教都打上门来了,c++的狂信者们,你们在哪里?你们对神的虔诚,哪里去了?
可惜,在我们,乃至世界上无数程序员眼里,这并不是——如同本版闹事者所以为的那样——宗教战争。
我们在实践中,早已发现c++的弊端。自己独立设计、编写的代码越多,这弊端也就越发明显。
比如,拿最容易说清楚的内存池来说吧。
如果用C写,很简单,void * pool_malloc(pool*, size_t)和void pool_free(pool*, void *)即可:用户拿去做什么,我不管;我在里面如何验证安全、加越界检查、内存泄漏检查,用户也不知道。
更进一步,如果想存特定大小的海量小对象,该怎么办?
增加个init_pool(pool*, size_t)接口即可。我在里面可以什么都不做,也可以切换到针对被管理单元大小一致优化过的方案上。
这就是所谓的 内存管理对业务透明,或者说 内存管理逻辑和其它业务逻辑正交。
这种模块非常独立,好写好测好掌握变化。
但,换了C++,怎么办?
首先,malloc来的内存,c++不能直接用。因为你不能调用构造函数(没错,C++阻止不了你强制调用构造函数,但这并不能正确完成初始化)。所以,你需要placement new。
placement new很麻烦。不仅是申请时需要指定从哪里分配,释放时也要指定由谁回收。而这,显然是麻烦的根源。
另外,因为内存池越是专用,就越容易设计的好;做通用了,性能反而不会比直接用原生内存管理来的高。
所以,为准备放到池中的类设计自己的operator new,就是更好的选择了。
但是,你必须知道,这个operator new并不能被子类直接拿来就用,因为子类往往总会比基类大一点点。加上内存池的专用性,或许你要用非虚析构函数来阻止由它继承——而非虚析构函数=禁止继承这点,很多程序员并不知道。
或许,直接使用STL的trait机制是更好的选择: 针对特定对象,指定给它特定的内存池好了。
不过,我想这个trait声明拿出来,那些出来打圣战的C++狂信者,多半是看不懂的。
另一个方面,如果规定所有被管理类都提供init方法,这个方法可以把自己重置为初始状态,那么这样做的内存池也非常棒:可以针对固定大小优化;不考虑所存对象细节;接口简单、朴素。
当然,你可以争辩说,这是把复杂度丢给了使用者。
你看,就这么个内存池,你得和多少其它特性打交道;为了合理设计它,你得多考虑多少东西。
这就是“非正交语言特性”带来的心智负担:用C写的池早已轻松支持写越界检查、内存泄漏报告了;C++还在纠结内存池是否需要知道所存对象的类型、用不用placement new、如何阻止重载了operator new的类被继承等等方面。
甚至,我还见过一个dephi写的游戏支持库,用的是最基础的pascal,水平非常高。他的内存管理实现了垃圾自动回收、对象池等等功能;核心代码也就几百行。
用这个库,你可以轻易为指定某个游戏物品执行一个动画:只要你产生个动画对象赋给它就完事;动画执行完了或者被其它事件中断了,这个对象会自动被回收。
这种东西,用C++来做——做到库的标准,也就是必须和其它特性互不冲突,或者有冲突可自动探测并触发编译器警报——就没那么容易了。
难度不仅仅在于代码量容易膨胀,还包括代码是否正确:因为这个池并不是一个单纯的池,它必须考虑其它对象怎么用它!
好在,我是个很实际的理想主义者。
当初写那么多模板、面向对象的东西,我知道自己没精力没时间去理顺它们和其它诘屈聱牙的东西之间的关系。
越是知道得多,这种无力感越是强烈——要能像印度程序员那样,一个星期只写200行代码,我可以做到滴水不漏。
但一天几百甚至数千行原创代码……我还真不敢保证。
对策是: 我用C++/STL写代码,但不提供C++/STL的接口!
我会清晰定义模块边界,然后用一组简单的C函数对外提供服务。这样,我才可以控制,保证使用者不会捣毁一切。
如果必须提供C++类/STL接口,那么我会提供最简单最朴素的,一点花巧没有。这样,我才可以保证,使用者随便怎么玩,都不会超脱我的控制范围。
即便如此,STL实例化时,不同编译单元里会出现同一静态对象的多个副本,仍然在控制范围之外。
更可怕的,模块外部是稳定了,其内部,仍然要考虑维护问题。
而我,并不能保证后来的维护者,不会用诘屈聱牙的东西来挑战我的逻辑。
我用大段的注释、说明指出可能的隐患和规避办法。因为我实在没有时间/精力用C++本身的机制一一堵住所有这些隐患。
——可是,为什么我要在本身已经足够良好的模块设计之外,不屈不挠的花费诺大精力为C++本身的非正交特性们擦屁股呢?
这让我很怀疑,C++是不是走错路子了。或者,至少是,在中国广为宣传的种种C++技巧,走错路子了。
Torvalds的劈头盖脸甩给cpper的bullshit,一度让我非常恼怒。但冷静下来后,我知道,恼怒只是因为被他说中了。
事实上,如果定义好模块边界后,可以用C提供接口的话;模块内部的子模块,子模块内部更小的功能模块,一样可以用C来提供接口。
而这种简单可靠的接口,可以让程序编写者忽略很多东西——诸如new和c原生数组的非正交、new和内存分配方式的非正交、模板和类的冲突、类和拷贝构造函数/自动类型转换、以及运算符重载前后的运算规则等等等等:我要的是功能,要的是便于使用的稳定接口,这些都干嘛啊过来瞎掺和?
于是,我逐渐在自己的模块内部,根据清晰的模块划分,以C或最简单的C++类方式提供接口;只有确认引入特性不会产生棘手的side effect时,才会利用那些“高级”特性。这让我少写了很多东西,少考虑了很多额外的东西。
久而久之,我拒绝用C++/面向对象的方式思考问题,拒绝把华丽的特性纳入考量范围。尤其是模块级别。
我很庆幸。当我见到tovalds丢来的一坨bullshit时,没有见到那么多的圣战众。
否则,也许还要走更多的弯路,才会回头。 |
|