关于C++“加、减机制”的整理
今天上C++的课,杨老师提到C++继承是“加机制”的,而没有像人类进化一样采取的是“减机制”,这样会导致代码的膨胀和冗余。回来后,特地查阅了一下资料,发现这方面的文章很少。
下边的资料摘自网上及杨老师学生所写的一篇关于“减机制”的思考,仅供参考。
1. 《仿生学在C++中的应用》
文章《仿生学在C++中的应用》(文章链接)一文就C++缺少"减机制"所带来的问题进行了阐述,现摘录如下:
“C++中缺少减机制,导致物种繁衍出现阻碍。C++是一门面向对象的语言,相对的面向过程的C语言来说,其主要特点就是类的应用。类在C++种的地位就相当于现实世界中的物体,而过程则相当于工作。有了物体,C++就能够模仿现实世界进行繁衍和进化,从而创造出更新的东西。
C++的初衷可能就是这样思考和设计的。但是用过C++的同志们都清楚,C++种的继承和派生是有问题的。比如基类共有3个整型变量,分别是public,protected和private,共有12字节。派生类的继承方式是public,此时派生类中的private变量已经不能够访问了,但派生类的长度仍然是12字节。如果派生类再加入其它变量的话,那整个空间占用量就会增加,而没有办法减少。
虽然这种越来越大的类会占用更多的空间,但这只是次要问题。如果从体系的角度考虑,就会发现C++的这种弊端会妨碍新物种的出现。大家都知道人类的继承是通过DNA分裂,然后从父亲和母亲各自拿一半,然后组成新的完整DNA。这样才会有变异和发展。然而,C++的单纯加机制导致的现象是物种只能发展而不能退化,这样思考的话就会出现带尾巴的人和带翅膀的牛羊。显然这是很不科学的。
如果今后能有一门语言,能够克服C++的弊端,让计算机的程序既有加机制也有减机制,符合生物进化论,让程序自动演化和繁殖,相信人工智能会先前迈进一大步。”
2.《C++继承机制中“减机制”不可实现的一个证明》
文章《C++继承机制中“减机制”不可实现的一个证明》(文章链接)则对“减机制”无法实现进行了证明:
“引言:
继承是面向对象程序设计支持代码重用的重要机制,有人更甚至认为继承是面向对象程序设计的全部。这种观点也许过于激进,但却也说明了继承在面向对象的程序设计中扮演的重要角色。确实,在C++语言中,通过继承,可以让一个类拥有另一个类的全部属性,换句话说,也就是可以通过一个已有的类派生出一个新类。新派生出的类也叫子类或派生类,而被继承的类则称作基类或父类。通过派生,可以实现代码的重用而大大节省相应的空间。
“减机制”问题的提出:
从上面我们对继承的说明,不难发现,子类在继承中不得不把父类的全部属性都继承过来,由于子类在继承的基础上还可以自定义新的属性,也就是说子类可以在继承所得的属性的基础上再加入新自定义的属性,这样,从父类到子类,也就实现了属性的“相加”和“增加”,我们姑且把C++中这种继承机制称为“加机制”。加机制所拥有的优点是不言而喻的,问题是,上述的继承过程可以反复进行,派生出多代的子类,而每代子类都可以加进新的属性,这样到最后一次继承时,被继承的类由于继承了前面所有类的属性而拥有了众多的属性(因而代码也变得异常庞大),并且其中的较多属性可能是他的子代的类所不需要的(但它的很多属性又是新的子代类所需的,因而必须继承它),这样我们自然而然会问一个问题,在继承过程中,可否有选择地只继承那些我们想要的属性,这样不仅可以得到我们需要的属性,也可以去掉那些我们不想要的属性,实现真正意义上的“简约”?由于这种全新的继承方式不同于我们上面提到的“加机制”,我们暂且称其为“减机制”。鉴于“减机制”相比“加机制”拥有更好的特性,对其是否可实现的探讨也就变得非常有必要。鱼和熊掌可兼得否?我们下面的证明将告诉你,天上确实不会掉下馅饼!
“减机制”不可实现的证明:
由于C++语言中类的属性的实现一般都是通过函数实现,因此我们只要证明在继承过程中函数的“继承”不可实现“减机制”,也就是证明在父代类里所拥有的函数,在子代的类里一定也拥有,那么我们也就等价地证明了继承的“减机制”确实不可实现。
对于实函数的情形,我们用B代表一个C++程序中的可执行的实函数,并记该函数的入口对应A,出口对应C。在继承时,一般情况下编译时的顺序为A→B→C(见图1),这样即可保证该函数被继承到新的类里,上面这个过程对一个类的所有函数都成立,因而一个类里的函数都可通过继承而全部进入另一个类里。对多重继承的情况,不同的亲代类的函数同理也可都在子代得到继承,也就是说,只要亲代的类里有的函数,子代的类通过继承都会拥有,这样,多个亲代的类的函数在子代类那里实现了“相加”,也即我们上面所说的“加机制”。下面我们将证明,“减机制”的不可实现性。
我们采用反证法,假定“减机制”可以实现,于是,不妨设某个亲代的类,如Class CBed里有一个可执行的函数B,在继承过程中,没有被子代的类所继承,也即在程序运行中,程序运行到函数的入口A后,直接就跳到了该函数的出口C处而没有经过B,这里我们又分两类情况讨论:①从入口A跳到出口C而不经过函数B是无条件的;②从入口A跳到出口C而不经过函数B是有条件的,下面我们将分类讨论。
对于①里的情况,由于程序在运行的过程中函数B在cpu里的编译和存储情况是一样的,也即我们在平常(非继承时)单独调用Class CBed里的函数B或在继承Class CBed过程中,函数B在编译器中的运行方式都是一样而不可区分的,如果在继承的过程中,运行到函数B时直接由A无条件地转到C(见图2),那么在通过Class CBed而直接调用函数B时也就会直接由A无条件地转到C,从而也就等价于Class CBed里根本没有B函数,这与我们的前提B函数是Class
CBed的一个可执行函数相矛盾,从而说明继承中“减机制”不可能实现。实际上,如果我们在继承过程中跳过一个函数,这其实等价于这个函数此时实际根本就不存在,从而也就无从谈对它的继承。以上情况的一个很好的例子就是我们直接用符号/* */将函数B的代码在原代码中直接屏蔽掉。
对于②里的情形,从A直接跳到B是有条件,也即在函数B的入口前有一个判断的环节,我们记为D,设其入口为A’,在一定条件下从A→C不经过函数B,当条件D不成立时,从A→C会经过函数B,这样经不经过B也就完全由判断环节D决定,而且其中的判断条件D可以任意更改,但对每一次完整的程序运行来说,这个条件是不可更改的,也即不允许在程序运行过程中对此条件作人为的改动。这个过程可以用图示简明地表示如图3所示。下面我们证明这种情况也是不可能实现的。假定上面的过程是可实现的,也即在条件D成立时,子代的类不会继承函数B,因我们只假定子代不继承函数B,这样函数B以外的其它部分,子代都要原样继承,那么子代在继承过程中,必然会有从A→C的这一环节,只不过这一环节不会经过函数B。因为此时判断条件D的出口和函数B的入口A是固联的,因而如果我们在这一次程序运行中,直接通过Class
CBed调用函数B,那么由于此时cpu对代码的编译运行跟继承过程中相似,那么编译的顺序仍为A’→D→A→C,这样我们也就无法实现对函数B的调用,也就相当于这种条件下,Class CBed中根本不存在函数B,这与我们的前提B函数是Class CBed的一个可执行函数相矛盾,从而,我们也就证明了②的情形下,“减机制”也是不可实现的。
综合上面的讨论,我们就证明了对于实函数的情形“减机制”继承是不可实现的,对于虚函数的情形,对于亲代类里的虚函数,因为它们的运行实际上是由一个指针在操纵,因此,我们只要把上面实函数情形里的实函数定义为对一个指针的操作,这样就把虚函数的情形也包括进来了,正如我们上面已证明实函数的“减机制”继承是不可能实现的,我们也就证明了虚函数的“减机制”继承也是不可实现的。
结语:
“减机制”的继承方式相对目前的“加机制”的继承方式虽然拥有更好的特性,但是它其实不过是美丽的空中楼阁,虚幻的乌托之邦,在现有的编译方式下是不可能实现的。若想要实现“减机制”,我们就不得不对C++语言进行内科手术而改变其目前的编译原理,但由于正如我们在上面的论证中所看到的,对函数要实现屏蔽,则必须在编译前事先知道它的入口和出口,而这一点是现有的编译原理所不能解决的(因为我们无法预料一个函数的长度从而知道它的入口也无法立即知道它的出口,除非先对这个函数进行编译,但这就和我们想达到的目的相矛盾),我们当然可以考虑给每个函数的入口和出口都做上一种编译器能直接识别的标志,这样确实可以实现“减机制”,但由于这项“手术”工程过于浩大,是否值得投入实践尚待进一步的研究。”
3.杨老师学生关于“减机制”设计的设想
而杨老师的一个学生则就实现“减机制”提供一些设想:
“杨老师经常强调的,作为一个面向对象的程序语言,应该尽量模仿自然。但是由于C++中的“类”没有‘减机制’造成无法实现模仿自然,于是需要引入该机制。在这里我感觉呢,这个‘减机制’,应该最终是体现在成品,也就是编译完可用的程序上的,而不是源代码。如果类比自然界,就是体现在生物个体的形态习性等上,而不是体现在其DNA序列上。这样考虑的话,实现这个“减”机制,我感觉可以在代码上的“不减”的基础上,来实现程序功能上的“减”。而这个实现过程,也许可以通过修改类中的字段赋值来实现。
好比老师上课讲过的那个例子。古猿到人类的进化中尾巴的问题,人类没有尾巴,但是人类的基因中依然有控制尾巴生长的基因存在。类比到C++,可以看成是“古猿”这一类派生出的‘人类’这一类中,在增加了许多人类特有的字段的同时,‘尾巴’这一字段并未减掉,而是赋值成为‘0’。这样的话,就实现了虽然代码上没有“减”,但是由于类本身在使用时都是要被‘特化’(自己编的词儿……就是说一个类在使用的时候总是要跟据情况赋值的)的,通过在特化时固定或者默认指定字段的值,就可以实现在类或者程序功能上的“减”。出于类比自然界的角度,这样设计的一个合理的地方,举例来说的话:假如某一天环境突然变了,需要人类再长出尾巴,由于这段基因还是在的(并没有在代码的层面被‘减掉’),那么人类适应这个环境变化的时间相比“无中生有”的产生一个控制尾巴的基因,再长出适宜长度的尾巴的时间要少得多。
这里随之出现的问题就是,如果要通过改变字段的值来实现程序功能上的‘减机制’的话,如何设计类能够使得这种操作变得方便,并且那些被固定或者默认设置的字段的值如何体现出它们与其它字段的特殊性就成了需要解决的问题了。继续用老师上课举的例子,如果‘重装骑士’中的字段设置是‘马的种类’、‘长枪形态’、‘盔甲类型’等等的话,这样在派生出‘坦克’时就会出现一个问题,就是马的种类无论如何设置,都是有马存在的,而且武器和盔甲字段也会出现类似的问题。这就是在设计类的时候没有考虑到为了实现前文提到的‘减机制’的运用。这里如果修改一下类的设置,同样是为了表现马,长枪和盔甲,可以设定成‘动力源’、‘破坏手段’、‘防御手段’三个字段,这样当其被赋值成,马,长枪,盔甲时,就是重装骑士,被赋值成柴油发动机,加农炮,复合装甲时,就是坦克。即是说,为了通过这种手段实现‘减机制’,必须在类的设计上下一番功夫,或者说需要一套设计标准或者对类的分析标准。这样就可以最大限度上体现出‘进化’并且也会出现在功能和效果上与原来的类完全不同的新的类。
不过,把这样的思考极限化就会带来一个问题:冗余。
即是说,如果这种思路可以的话,很有可能会出现一个类中有很多很多的字段,但是在某一特定应用中只能利用到很少很少的一部分,如此庞大的冗余在现今的程序设计中是可笑而且不能被允许的。在这里再次考虑面向对象程序设计语言要对自然界进行模仿这一初衷的话,我感觉就能得出一个矛盾。这个矛盾就是,程序设计以减少‘冗余’为追求目标之一,而自然界许多东西就是有很庞大的‘冗余’的。较容易理解的就是用基因和生物的关系对比代码和具有特定功能的程序的关系。我翻了一些书,请教了些同学,结果是,拿人类来说,基因中有据说98%以上的基因对于现阶段的人类来说是无用的,没有表现出来的,即所谓“无用基因”。这样庞大的“冗余数据”对于现阶段的程序设计是不可想象的。因此,这个矛盾决定了,在如今任何程序都是要考虑的节约资源的情况下,是不可能出现真正模仿自然界的程序设计语言的,无论程度如何,因为二者在指导思想上就是南辕北辙的,无论引入任何机制,都是治标不治本的。
这个矛盾的出现无疑是由于自然界的生物所要面对的变化之复杂程度,实在是超出我们设计程序所遇到的情况太多了。致使,对于自然界来说,这些‘冗余’是完全有必要而且很重要的资源,因为很有可能还能使用上,如果把那些‘冗余’扔掉的话,很有可能下一次环境变化就会出现种群灭亡的危机;而现阶段的程序设计就不存在这么急迫的问题,我们完全可以只利用现在用得着的字段,而扔掉暂时不需要的字段,即便出现了之前被扔掉的字段重新被需要,我们也不会感受到在生物界的‘种群灭亡的危机’,所以我们完全可以以我们可以接受的代价来重新设计类。
但是这又说明了另一个问题,即,现阶段人类的对自然的模拟都是很低级的,也即使说技术和需求都是很低级的。那么,上述‘减机制’的操作方法还是有一定的空间的,因为首先我们就不会用计算机去‘完美’模仿任何自然界现有实物,正常人是不会有这种用需求存在的,一般都是很精简化的模仿。这样就已经减少了很大一部分问题。而冗余问题,自然也得到相应的减少。同时通过合理的规程应该可以在一定程度上更有利于实现上述减机制。比如还是那个骑士和坦克的问题,如果按照前一种字段设定方式,当派生出坦克时那么我们就必须让‘马的种类’、‘长枪形态’、‘盔甲类型’这三个字段赋值为‘0’或者类似的什么,并添加坦克的相应字段,而如果此时再没有考虑字段设定的规则的话,当派生出新的类时,又会出现一样的问题。这类的冗余是不应该存在的、真正的‘冗余’,是可以通过合理的规则避免的。那么综合上面的考虑,在现阶段需求并不高的情况下,这种‘减机制’的实现方式还是可以使用的。
我的想法总结起来就是:
‘减机制’的引入应该体现在程序的效果和功能层面上而不是代码层面上;
类上的‘减’机制可以通过修改类内的特定字段的值实现,从而达到尽管在代码层面上是“加”机制,而功能效果层面上是“减”机制;
实现该方法的途径之一,就是制定一套类的功能分析规程和类的描述即字段设定方法;
现阶段程序设计的指导思想与自然界的矛盾决定了不可能出现模拟自然界的程序设计语言;
但也正由于现阶段对于模拟自然界在技术和需求上的较低级,才使得在一定范围内模拟自然界一些方法有了使用的价值。”