被误解的C++——C++的缺陷和D的缺陷

C++的缺陷和D的缺陷

D语言,从字面上讲应当是在C/C++的基础上进了一位,其特性当然也进了一位。真是这样?也是,也不是。这得看你的出发点和价值观了。

D的定位在于继承C/C++的优势,但却更加易学易用。这种定位招人喜爱。C/C++的致命伤就在难学难用上。(不少人认为C++难学易用,我也持这种观点。但是既然要拿来同D比较,那我也只能跳一回票。只此一次,下不为例)。

我大致了解了一下D的特性,初步混了个脸熟。D差不多通过以下方式达到其目的的(如有错误或遗漏,请轻轻扔砖J):

1.       新的语法,消除了一些缺陷,比如C++模板的>>问题(对此我持保留意见,一会儿再探讨)。同时,也优化了编译,提高了编译速度;

2.       将字符串、数组等内置,使其得到编译器的充分支持;

3.       module代替头文件,抛弃了来自远古时代的遗物;

4.       用内置特性实现C++的一些库功能,比如traits

5.       使用gc方便内存管理。但保留RAII功能;

6.       增加编译时计算支持。将C++部分通过模板实现的编译时计算放到内置特性中实现;

7.       增加mixinmp支持。

限于我对D的了解,暂时只想到这些内容。总体上,可以认为,D的改进主要集中在将C++的很多由库实现转移到内置特性实现。内置特性的实现通常具有针对性,在语法上更简洁。此外,D也消减了一些不常用,但容易引起麻烦的特性。并且使一些特性自动化,以减少使用时的压力。

但在我看来,D所做的,是好心没办好事。因为D并没有真正抓住C++的核心问题,也没有很好地认清C++成功的关键。D继承了C++大部分的特性,这表明它认可C++的强大。(不然它留那么多特性干什么)。但是却没有能够从C++身上吸取真正的教训。

首先,C++的语法存在缺陷,Bjarne不止一次提到这个问题。根本原因可能是其语法是non-LR的。这个问题应该不难解决,我相信D已经解决了。但是,D大费周章地采用()!()作为模板的操作符,似乎有欠妥当。首先,程序员的习惯,促使他们不容易接受这个新操作符;其次,()容易同函数参数产生混淆,不如<>来的直观;最后,过度放大<>存在的问题。<>的问题主要有两个,一个是同>>操作符冲突,这个问题在C++0x中已经解决。另一个是<3>4>问题,这种情况相对少见,而且可以利用<(3>4)>加以解决,并非关键性问题。为这些“鸡毛蒜皮”的问题而采用(),似乎不太值得。

其次,C++的最初动机也就是提供“更好的C”。此后,随着需求的不断扩展,逐步加入了各种高级特性。也就是说,Bjarne在最初的那段时间里,并不是那么高瞻远瞩。尽管他为C带来了OOP这种新的编程范型,但是从最后的结果来看,他的思维似乎也不够大胆。因此,C++的很多特性配合不好,有拼凑的感觉。D则站在了巨人的肩膀上,自然有更好的基础,更容易避免C++身上发生过的问题。

再来看D,目前所有的特性和能力,都没有超出原来C++的范畴,只是在其上做一些小修小补。从编程技术而言,相对C++没有显著的进步。不错,D在很大程度上简化了使用,但这是有代价的,它放弃了很多有用但难缠的特性。但这也会带来应用上的局限。

对于系统编程的定位,D似乎把易用性放在了过高的位置。实际上,后面我们将看到,D的面前同样存在另一条路可走。这条路,可以在C++的基础上提供更好、更强大的特性,但却可以消除很多C++的不足(但不是所有的,很多问题算不上缺陷,只是某种强大功能的副作用。强大的功能人人都想要,对吧?那就忍着点吧)。

再次,C++的很多问题来源于它逐步堆砌的特性。而且这些特性又是来源复杂,缺乏一致性。由此,很多first-class的概念被迫使用非first-class概念实现。

在这方面,D则做得更糟。比如,D强化了gp,但却没有引入concept,将来是否会引入,尚不得而知。这使得D无法根本上消除gp中遇到的诸多问题。相反D试图通过static ifstatic dispatchC++常用的手段)之类的second-class特性完成concept这种first-class的需求。显然没有从C++中获得教训。

D象其他语言那样,把数组和字符串作为内置类型处理。在表面上,这是进步,用first-class的概念,用first-class的类型表示。但在我看来,这是一种倒退。说清这件事,就得看看什么是first-class的。传统上,类型分为内置类型和用户定义类型。而内置类型被界定为first-class的,得到编译器的优先照顾。然而,内置类型是否真的first-class呢?不,有比内置类型更first-class的,那就是类型。迷糊了?我慢慢说。

无论内置类型,还是用户定义类型,都是类型。我们传统上将他们区别对待。实际上,划分内置类型和用户定义类型的一个理想上的标准是原子性,即如果一个类型,(在语义上)不能分割了,便作为一个内置类型处理。为了保持移植性,象int之类的类型,被作为原子类型,而不考虑它内部字节的排列。同样stringarray也是如此。但是,在很多系统中,比如SQL,为了处理方便,很多非原子性的类型也作为内置类型处理,比如datetime。这就表明,现实中内置类型和用户定义类型并没有严格的界限。

C++创建时有一个宗旨:让用户定义类型象内置类型一样处理。这句话是非常first-class的。也就是说,把所有类型一视同仁。如果能够做到,那么这门语言中,将只有类型这样一个first-class的概念,而不再分类。那这样得到的好处是什么呢?就是灵活性、扩展性、简洁性。

下一个问题,哪种类型是最first-class的?在C++中,内置类型是最first-class的,D也是一样。但事实上,最first-class并非内置类型,而是字节。汇编语言中,所有类型都可以看作字节或字节序列。也就是说,任何类型,无论内置类型还是用户定义类型,都是由字节组成。在此基础上,我们便可以用字节定义类型的静态结构,包括内置类型。也就是说,在静态结构定义上,内置类型完全可以同用户定义类型同等处理。

但是,内置类型和用户定义类型的行为是不同的,因为编译器并未把他们作为同一样东西处理。此时,我们便可以看到C++那句豪言壮语的作用了。当用户类型能够像内置类型一样的情况下,内置类型和用户定义类型将不做区分,完全可以一样处理。这便可以导致一个结果,就是所有类型,包括内置类型,都可以通过库而非编译器实现。于是,语言便可以扩展出丰富的“内置类型”。(呵呵,是不是有点metaprogramming的味道?DSL听了肯定高兴)。

C++朝这个方向努力做了,实现了绝大部分目标,但也未能100%地实现。比如,智能指针尚无法做到象内置指针一模一样的行为。(C++0x引入三个cast操作符重载后,情况就会好很多)。

这一点上D走出了一步,但却没能乘胜追击。D为所有类型,包括用户定义类型都定义了一组properties,用于获取类型的特性。在这一点上,所有的类型都一视同仁。但是,之后却依旧按传统将内置类型和用户定义类型隔离,并且将数组和字符串放回内置类型。再加上操作符重载上的一个明显退化,便坐失了统一类型处理的优势。

操作符重载,是使用户定义类型得以同内置类型一样处理的关键。C++除了少数的几个操作符外,其余都可以重载。这带来了极大的灵活性,(尽管如此,也未能使所有类型统一处理),但也带来了一些麻烦。(操作符重载带来的语义上的问题,不应该由语言负责,除非不想获得操作符重载的好处,否则必须承担此中的风险)。D缩小了重载的范围,避免一些混淆,以达到简化学习和使用的目的。(C#也是如此,但我并没有看到有多大的效果,我们重载操作符通常都集中在某些常用的操作符上,多数的操作符重载不会成为困扰我们的原因)。

D在操作符重载上的一个明显的变化,就是不使用操作符本身作为重载的定义,而是用等价的字符表示:

//C++

A operator+(A const&, int);

//D

class A

{

       A opAdd(int v) {…}

    int opPos(){…}

};

这种方式存在它的好处,但同时也带来了不足。好处是可以明确地区分统一操作符在不同情况下的语义,看起来非常明确。但是,这种方式使用起来却不如直接使用操作符本身来的直观,使用者必须识别这些操作符和对应的函数,这增加了记忆的负担。而operator+则侧重于记忆少数规则,理解规则,便可以举一反三,快速运用到其他操作符上。同时增加的这些内置函数记号,消耗了宝贵的关键字。两者的是非得失,全凭使用者的喜好,算不上什么天大的问题。只是我更喜欢记忆规则,而不是具体的记号。

D的操作符重载真正的问题在于取消了自由函数的重载形式。这样似乎简化了学习和使用,但实际上,只会把问题复杂化。成员型的操作符重载并非操作符最本质的形式。将操作符重载完全局限于成员函数,使得操作数无法交换。于是D通过允许定义opxxx_r()程序函数定义交换操作数的版本(复杂了吧)。对于另一个操作数类型,是否定义相应的操作符重载呢?这种方式迫使程序员不断地考虑这类问题。如果合作开发,那么必然增加了程序员间协调的负担。

无论如何,二元操作符的形式更接近自由函数。用自由函数加以表达,更加直观和明确,而且更便于集中处理。这篇文章,很好地阐述了过多的成员函数对封装性产生的影响。成员函数的一个好处是可以访问非public成员,但就像其他成员函数一样,成员型的操作符削弱了类型的封装性。如果不访问非public成员,那么作为成员,没有任何意义。

操作符重载是一个first-class的特性,但是D却使用了非first-class的形式表达。作为对C++操作符重载的简化,我认为合理的形式应当是:赋值操作符(=)、类型转换操作符,以及所有一元操作符,都采用成员的形式。二元以上的操作符都采用自由函数的形式。这样,规则并没有复杂多少,但却使得不同的操作符重载都能够以first-class的形式表示。

first-class特性的问题上,另一个问题是D引以为豪的traitsTraits是非常有用的东西,使我们静态地获取类型的特征。traitsC++中起了非常重要的作用。所不同的是,C++通过类库的形式提供traits,而D则采用编译器内置特性提供。相比之下,受制于语言本身的特性,类库的实现难以提供完全的类型信息。而编译器内置特性,可以提供更完全的内容。

但是traits毕竟是非first-class的特性,C++0x通过引入first-classconcept,大大消除了对traits的依赖。具体的可以看这里这里这里。而traits背后真正的机制则是reflection。在这里reflection是真正的first-class机制,而traits是缺乏reflection(编译时)的一种替代或模拟。

D的问题在于,traits成为一种语言特性,当未来引入了conceptreflection之后,它将成为摆设,宝贵的语言特性资源被浪费。如果不引入conceptreflection,那么很多依赖于这些特性的问题无法得到解决,而traits也无法独立支撑这个局面。这会使D在这方面处于进退两难的境地。同样的问题也存在于mixin上。Mixin可以被看作一种Meta-Programming的机制,但并不完全,也非first-class特性。为真正解决现实中的问题,在GPL中引入Meta-Programming的需求越来越强烈。一旦引入真正的first-classMP机制,那么mixin也会处于尴尬的地位。

这些问题表明,设计D的出发点存在问题。D仅仅试图在C++的基础上简化学习和使用,而不是采取更加本质,更加根本,更加first-class的手段来彻底解决C++面临的问题。就是说D并非一种面向未来设计的语言,仅仅关注眼前的蝇头小利。这种思维上的局限性,很容易使D在未来同更强大的语言,不仅仅是未来的C++,竞争的时候,处于不利的地位。因为限制已经造成,围栏已经建好,再想扩展便会受到很大的限制。

我还是那句话,如果D仅仅局限在修正C++的某些问题,那么说明它并没有从C++哪里吸取真正的教训

最后,D似乎并没有充分意识到灵活性和程序员的选择对系统开发的重要性。C++的机巧性、危险性很大程度上是被过度放大了。在现实的开发过程中,我们绝大部分的时间,实际上都在老老实实地使用C++的普通功能,(但请记住,是在高级特性的支援下,使用普通功能)。这些动作都是常规的,成熟的。至于那些复杂、危险特性,则很少使用。即便使用,也是由专人(受过训练的,有免疫能力的)集中运用。在这样一个大环境下,语言的灵活性相比那些很cool的功能而言,更加重要。比如,使用多继承的权利。

多继承的主要问题在于钻石型继承带来的麻烦。但是,这种情况极少出现,通常也只在过度OO的设计中存在。在其他方面,多继承是非常容易处理和使用的。更重要的是,它是非常有用的,有时甚至是关键的(想想policy可以为我们消除多少类、继承和代码冗余,带来多大的灵活性)。为了一种很少出现的情况,把路整个地堵死,对于java这样的高层语言,或许可以忍受,但对于系统级的语言,是难以接受的。(至少对我如此)。更何况编译器可以准确地识别钻石型继承,并作出自动化处理(默认virtual继承),除非有特殊需要。

系统级语言不仅仅要求能够完成程序。鉴于系统开发的广泛性、灵活性,以及扩展性要求,语言的灵活性和程序员的选择是非常重要的。作为高附加值的开发任务,系统开发对于语言的复杂性的容忍能力是非常强的。

C++的问题是过于灵活,比如缺省情况下单参构造函数执行隐式类型转换,在应用中造成无数问题。一门新的语言完全可以通过合理地限制这种灵活性,消除问题。

我并不是说,C++那些缺陷和复杂是正当的。我只是想表明,通过消减语言特性,抑制语言的灵活性,限制使用者的选择,不是简化语言使用的正当手法。一种系统语言,应当以更加根本(本质)的方式解决C++的问题。在这一点上,D做的并不是很好。(就像跑步比赛,如果只把目标定在第一个人的身上,那么终究无法将其超越。只有把目标定在第一个人前面,才能得到冠军)。当D只专注于宣扬C++的缺陷,以及它所给出的解决方案时,便注定它无法成为超越C++的语言。在我看来,它应当把更多的精力花在如何提供更多first-class特性上,而不是用来标榜自己的那一点点进步。

 
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值