Jump to Table of Contents Pop Out Sidebar

「恒虚之境」C++ 有多难

C++ 之难不在于其语法的复杂性,也不在于二进制层面上语义的杂乱无章,更不在于玄妙得不食人间烟火的模板推导(模板元编程),这些都只是表象。本质上讲,C++ 跟任何语言比,它很独特很怪异(废话,任何一种语言那个不特异)。

很多时候,C++ 给人的感觉就是,好像任何一种语言的特性(这话有点夸张),都可以在 C++ 王国中,通过令人发指的奇技淫巧,罄竹难书的花样作死,最后终于可以在一定程度上模拟出来,但是模拟后的结果,又总是存在这样那样的不足,要么因为内存管理,要么因为反射的原因,总之,就是好像可以做一切事情,但最后终于做得不好。这个时候,猿猴要么就直接扑上原生带有这种特性的语言,要么干脆就完全舍弃,放弃治疗,啥技巧也不用,返璞归真,就老老实实一行代码一行代码、不厌其烦、不畏枯燥地一再写地重复类似的功能。而 C++ 自身的优秀特性(析构函数、内存管理、模板、多继承等等),没有任何一种语言整的出来,当然,也可以说,这些玩意都是为了解决 C++ 自身制造出来麻烦,other语言s 完全不 care 这些杂碎。难道,这些好东西就没有一丁点价值了。

更令人难堪的是,迄今为止,C++ 业界就没有出现过方方面面都让人满意的基础库,也即是性能(对 c++ 猿来说,性能最重要)、可扩展性、易用性、安全性都经得起推敲。所有的通用库、流行库都存在这样那样的诟病,stl 如是,boost 如是,qt 如是,……。所以,就开始有人(现在是普遍都持这种观点啦,十几年前还只是开始)怀疑了,为什么其他语言出来不久,就马上配套相应的官方基础权威平台框架库,就算是 C 语言,也有标准头文件库,里面也确实没有可争议之处。就 C++ 诸多借口,迟迟交不出答卷。这一定是语言的问题,毫无疑问。就算不是语言的问题,你看看,几十年下来,多少大牛,就搞不出来的东西,由此可见,C++ 有多麻烦,有多复杂,平常人 hold 得住吗?

可是,基于 C 语言,C++ 多出来任何特性,都确确实实很有价值,用得好,的而且确,可以节省很多很多重复代码。就算最让人诟病的隐式类型转换,虽然一不小心,就给猿猴惊喜,带来理解上麻烦。但是,在可控的情况下,真的可以少写烦心的代码,某些场合,也是奇技淫巧的用武之地。好吧,既然 low C 都能写出来基础通用库,反而高大上的 C++ 就举步维艰了。问题是,精致的 C 代码,到了 C++ 舞台上,马上就备受一连串很有道理的指责,类型不安全啦,缺乏弹性啦,不够易用性,甚至连性能(明明在 C 那里就是极限了),也可以榨出汁来。既然你大 C++ 这么厉害,你行你上啊,少在这里瞎逼逼。几十年了,你换了多少次新马甲,依然虚有其表,金玉其外。可以想象大 C++ 的老脸有多红啊。

所以说,这个世界的猿猴语言分为两大类,C++ 与其他语言。其他语言稍微努力就能做出来很有群众基础的通用库(不接受也不行,压根就不让你做文章,就不给你后门更好地实现),进而跑步进入共产主义,人生苦短。而 C++ 看着其他语言的通用库,就会瞎逼逼,指指点点,这里不好,那里不对,但是自己无论如何,就是只能做出来小团体运用,自我陶醉的通用库,这些所谓的通用库,最后多半要被历史潮流所淘汰。

当你废了九牛二虎之力搞清楚了 C++ 的对象模型(继承、多继承、虚继承、异常、……),各种数据类型在内存中表示,还搞清楚不同编译器的不同处理方式;兴致勃勃搞起模板元编程从入门到放弃;预处理的伪图灵完备也玩出翔来;将 mfc 操得体无完肤;对 stl、boost 也深挖祖坟以至深感失望(stl 还好,boost 真心烂);C++ 编译器也被操得死去活来;……,这都有多少年过去了,依然感觉写不好 C++ 代码,依然充满疑惑,代码写来写去,总是感觉写的不对,似乎还可以提升,还可以提升,可以在牺牲性能、类型安全、弹性、易用性的前提下继续加强性能、类型安全、弹性、易用性,你不知道 C++ 的上限在哪里。直到有一天,那一刻,终于到达彼岸。

对于普通用户来说,C++ 最大的问题,就是缺乏一个高水准高质量的基础通用库,这个库,首先要坚持住零惩罚抽象的底线,不管怎么样,都不得妥协,历史的经验证明,在这一点上退缩的基础库,最后都将导致整个设计框架上的冗余,倒不仅仅只是因为性能的原因。其次,在性能、弹性、使用接口、类型安全等综合方面都要取到很好的平衡点,也就是说用这个库说人话的时候,其性能一定要不差,好用,有弹性,类型上用得不对,编译器就不满意。但是这个基础库又要像大 C++ 语言本身一样,充满后门,只要愿意,随时都可以为了提升某一要素,可以牺牲任何其他要素(比如说为了性能,将使用接口、弹性上搞得很难看)。具体展开来说,这个基础库包括,完备的内存管理(支持一定程度的 gc 效果,其实就是多次分配,一次统一释放);完备的运行时类型信息;极大地挖掘 template 的潜力;编译期与运行期的无缝对接,二进制语义上的清晰。至于再具体来说,比如字符串设计,格式化,序列化,IO,侵入式的容器,迭代器,协程等,就是细微末节了。

显然,以上述的要求来评判 stl,显然 stl 很不行的,虽然勉勉强强坚持住零惩罚的底线,但是总体来说,其性能、弹性、使用接口、类型安全的总体分数上是相当低的。1、竟然脑洞大开,将内存管理当做是容器的类型参数来操作,这样玩的严重可怕后果,就不说 stl 的东西不能很好地用于动态库,想要做有 gc 效果的内存管理也不好办了;2、对模板的使用只停留在很低的层次上,本来可以提供更多更有力的抽象机制,比如非侵入式接口,比如消息,比如异构容器;3、回避虚函数,回避反射,导致stl的运行时类型信息很薄弱,进而导致垃圾的 io stream 实现,对面向对象的支持极差,也导致痛苦漫长的编译过程;4、对于编译期的丰富类型信息,只会用类型拭擦一招带到运行期,导致到运行期时丢失了很多重要的信息;5、对于容器的二进制布局,回避,不花心思,所以二进制的复用效果很差;……,算了,对 stl 的不满,简直是说三天三夜也说不完。所以,以 stl 为基础来写代码,能不一再反复造轮子,写代码能愉快?

所以,C++ 的难,说到底,只是通用基础库的实现之难。C++ 的难,是 C++ 专家的难题,并非普通用户的麻烦。那么,实现通用基础库的难度有多大,从 C++ 面试至今三十多年,还没出现过,你说会有多难呢?那么,为什么会这么难呢?

一直以来,C++ 专家对 C++ 的认识,一直停留在很低的层面上。面对着 C++ 复杂庞大的类型系统,完全开放式的内存管理,直接操作机器的种种方便,威力无比的 template,很丑陋又好像很重要很有作用的预处理,厚颜无耻的多继承以及不定时炸弹的异常,这些东西造就了 C++ 无限可能的同时,也造成了 C++ 在打造基础通用的极大困难。表面上存在无穷无尽的选择,但是正确的路子,真心不多。一不小心,就面临无穷无尽的细节上的考究,最后造出来的轮子,反而引入更多的麻烦。而更糟糕的是,很多人更高估了自己对 C++ 的认识,贸贸然就随随便便造轮子,还大规模的在代码上到处泛滥。(待续)