huaxiazhihuo

 

再议c++的面向对象能力之上

C++的面向对象设计能力,与javaC#这两个杂碎相比,一直都是一个大笑话,现在谁敢正儿八经地用c++搞面向对象的框架系统,业界都用javaC#搞设计模式,那关C++什么事情了。而C++也很有自知之明,很知趣,98年之后,就不怎么对外宣称自己是面向对象的语言,就不怎么搞面向对象研究了(难道是c++下的面向对象已经被研究透彻?),一直在吃template的老本,一直到现在,template这笔丰厚的遗产,貌似还够c++吃上几十年。今时今日,virtual早就沦落为template的附庸,除了帮助template搞点类型擦除的行为艺术之外,就很难再见到其身影了。有那么几年,业界反思c++的面向对象范式,批斗virtual,特别是function出现之后,要搞动态行为,就更加不关virtual的什么事情了。而那几年,本座也学着大神忌讳virtual关键字。现在大家似乎已经达成共识,c++里头的面向对象能力很不完善,要玩面向对象就应该找其他语言,比如javaC#杂碎;或者更动态类型的语言,好比pythonRuby;或者干脆就是原教旨的面向对象(消息发送),object Csmalltalk

是啊,1、没有垃圾回收;2、没有原生支持的完善反射能力;3、多继承、虚继承导致的复杂内存布局。这三座大山面前,c++的码猿哪敢染指什么面向对象,只在迫不得已的情况下,小心翼翼地使用virtual。但是,事实上,要玩面向对象,c++原来也可以玩得很炫,甚至,可以说,关于面向对象的能力,c++是最强的(没有之一)。这怎么可能?

所谓的面向对象,说白了,就是对动态行为的信息支持,能在面向对象设计上独领风骚的语言,都是有着完善的运行时类型信息,就连lisp,其运行时元数据也都很完备。静态强类型语言(javaC#)与动态语言比,显然有着强大的静态类型能力(这不是废话吗),能在编译期就提前发现类型上的诸多错误,但是也因此带上静态约束,导致呆板、繁琐的代码,java的繁文缛节,就是最好证明;而动态语言恰好相反,代码简洁,废话少,但是丧失静态信息,所谓重构火葬场,那都是血和泪的教训。静态语言与动态语言真是一对冤家,如同光的波粒性,己之所长恰是彼之所短,己之所短又是彼之所长,鱼与熊掌不可兼得。而C++竟然能集两家之所长,在静态语言的领域中玩各种动态行为艺术,比如动态修改类型的反射信息,千奇百怪的花样作死(丧心病狂的类型转换);在动态范畴里面,又可以在编译期榨取出来静态类型信息,比如,消息发送的参数信息,想想win32的无类型的wparam和lparam,每次都要猿猴对照手册解码,从而最大限度地挖掘编译器的最大潜力。所以说,c++是最强大的面向对象语言,没有之一。而把静态和动态融为一体之后,c++的抽象能力也到达一个全新的高度,自动代码生成,以后再发挥,这是一个庞大的课题。C++令人发指的强大,绝对远远超乎等闲猿猴的想象,特别是那批c with class的草覆虫原始生物。C++只在部分函数领域的概念上表现令人不满,比如lambda表达式的参数类型自动推导,monad表达式,缺乏原生的延迟求值等。当然,c++整个的设计理念非常繁杂随心所欲,但是,却可以在这一块混沌里面整理出来一些举世无双的思想体系,就是说,c++是一大堆原材料,还有很多厨房用具,包括柴火,让猿猴自行下厨,做出来的菜肴可以很难吃,也可以是满汉全席,全看猿猴的手艺。

当然,要在c++里头搞面向对象,多继承,虚继承的那一套,必须彻底抛弃。最大的问题是,多继承会导致混乱未知的二进制内存布局,虚函数表也一塌糊涂,十几年前,c++设计新思维的基于policy的范式,虽然令人耳目一新,也因为这种范式下对象的内存布局千奇百怪,所以,即便是最轻微的流行也没有出现过。当然,也不可能大规模搞消息发送这种很geek的套路,功能太泛化了,其实,消息发送就是动态的给对象添加成员函数,并且可以在运行时知道对象有多少成员函数,那个成员函数可以对该消息做出反应,消息可以是字符串,整型ID(原子), MFC的消息映射表(BEGIN_MESSAGE_MAP…)就是一个功能严重缩水版的好例子,c++下支持消息映射的库,绝对可以比破mfc的那一套要好上千百倍,不管是性能、类型安全、使用方便上。目前除了在gui这种变态的场合下才需要大搞消息发送,其他场景,完全可以说用不上,虽然说消息发送很强大很灵活,但也因为其杀伤力太厉害,反而要更加慎重。这好比goto,好比指针,好比stl的迭代器,什么都能做的意思,就是什么都尽量不让它做。

那么,c++下搞面向对象,还有什么法宝可用呢?当然,在此之前,我们先要直面内存分配。内存既是c++的安身立命之本,又是c++沦落为落水狗丧家犬之幕后大黑手。假如不能为所欲为的操作内存,那么c++的折腾法子,奇技淫巧,起码要死掉一大半以上。而由于要支持各种花样作死的内存操作,c++的垃圾回收迟迟未曾出现,就连以巨硬之大能整出来的.net那么好的gc,霸王硬上弓,在给原生c++强硬加上托管功能(垃圾回收),都出力不讨好。可见未来垃圾回收,对c++来说,嗯,想想就好了。内存是资源,没错,用raii来管理,也无可厚非。但是,内存却是一种很特殊的资源,1、内存时对象的安身立命之所;2、不同于普通资源,内存很多,不需要马上用完就急急忙忙启动清理工作,只要系统还有大把空余的内存,就算还有很多被浪费了的内存,都不要紧,gc也是因为这个原因才得以存在。相比内存,普通资源给人的感觉就是数量及其有限,然后要提交工作结果,否则之前所做努力就废了。所以,对于内存,应该也要特别对待。就算raii,也要采用专门的raii

假设我们的程序里面使用多种内存分配器,比如说,每个线程都有自己专有的内存allocator对象,然后,线程之间的共享数据由全局的内存分配器分配,线程的内部对象都用线程的专属allocator来分配,那么,内存分配器就是一个线程局部变量(tlsthread local storage)。于是,可以规定,所有的内存分配都通过GetTlsAllocator()new对象,当然,确定是全局共享变量的话,没办法,就只能用GetGlobalAllocator()new对象。那么,有理由相信,启动一个任务时,我们先定义一个arena allocator变量,并令其成为当前线程的专属内存分配器,那么这个任务后面的所有new 出来的对象,包括循环引用,都不必关心。只要任务一结束,这个arena allocator变量一释放,所有寄生在它身上的对象,全部也都消失得干干净净,没有任何一点点的内存泄露。就算任务内部有大量的内存泄露,那又如何,任务一结束,所有跟此任务有关的一切内存,全部成块清空。总之,不要以常规raii来解决内存困境,解放思想,在内存释放上,我们可以有九种办法让它死,而不是仅仅靠shared_ptrunique_ptrweak_ptr这些狭隘的思维。

其次,完善的面向对象设计,避免不了完备的反射,用以在运行时提供动态类型信息,无参模板函数可以把静态类型映射成全局唯一变量,好比,TypeOf<vector<int>>,返回vector<int>的全局唯一的const TypeInfo*对象,这个对象包含了vector<int>的所有静态类型信息,可以这么说,在静态类型层面上vector<int>所能做的任何事情,比如定义一个vector<int>的变量,也即是创建对象;遍历、添加元素、析构、复制赋值、元素数量等等一切操作,与vector<int>对应的TypeInfo对象,统统都可以做到。所不同的是,vector<int>的静态类型代码,只能用于vector<int>自身的情况(这样子可放在源文件中),又或者是通过template,表现行为类似于vector<int>的数据类型(代码必须在头文件上)。而用TypeInfo*做的事情,全部都在运行时发生,所有的静态类型信息,全部被带到运行时来做,所以这些代码全部都可以处在源文件里面,甚至动态库里头,只不过是TypeInfo*操作的对象是一个二进制内存布局和vector<int>一模一样的内存块,可以通过强制类型转换,把运行时的内存块转换成静态编译时的vector<int>。其实这里的思想,就是想方设法将丰富多彩的静态类型信息无损的保存到运行时中,让编译时能做的事情,运行时也可以做。差别在于,一个是用静态类型信息来做事情,这里,任何一点点类型上的错误,都会让编译器很不高兴;一个则是用动态类型信息来做事情,这里,显然只能让猿猴人肉编译器。这里,可见动态类型信息和静态类型信息的表达能力是等价的,也即是同等重要性的意义,而静态类型信息的意义有多大,相信大家都知道。

那么,如何建立完备的反射信息,这个必须只能用宏来配合完成,外部工具生成的反射信息代码,功能很不完备,另外,c#java等的反射信息全部都是编译器生成的,可定制性很差。我们需要的是一点都不逊色于静态行为的动态行为。所以,只有由自己自行管理反射,才能做到真正意义上的完备反射。必要时,我们还可以在运行时修改反射信息,从而动态地增删对象的行为方式,改变对象的面貌。看到这里,是否觉得很多的设计模式,在这里会有更清晰更简洁的表达方式呢,甚至,轻而易举就可以出现新的设计模式。比如,以下定义对象反射信息的代码。

c++下,由于全局变量生命周期的随意性(构造函数调用顺序不确定,析构顺序也不确定),大家都很忌讳其使用,虽然全局变量功能很强大,很多时候都避免不了。但是,标准上还是规定了全局变量的顺序,所有的全局变量必须在main函数之前构造完成,其析构函数也只能在main函数结束后才调用。另外,函数的静态变量必须在其第一次访问之前构造完整。基于这两点,我们就可以在main函数之前构建全部的反射信息,流程是这样子,所有的类型的反射对象都是以函数内部的静态指针变量存在,他们都通过调用GetStaticAllocator()的内存分配器来创建,这样子,提供反射信息的函数,就避免了其内部TypeInfo对象的析构发生。最后,main结束后,由GetStaticAllocator()函数内的内存分配器的析构函数统一释放所有反射信息占用的内存。最后,附上一个例子

    struct Student
    {
        
//ClassCat表示为Student的基类,为空类,所以Student可以继承它,但是代码上又不需要明确继承它,非侵入式的基类。
        
//ClassCat提供二进制序列化操作,xml序列化,json序列化,数据库序列化等操作
        PPInlineClassTI(ClassCat, Student, ti)
        {
            PPReflAField(ti, name);
            PPReflAField(ti, age);
            PPReflAField(ti, sex, { kAttrXmlIgnore });    
//表示不参与xml的序列化操作
        }
        AString name;
        
int age;
        
bool sex;
    };
    
struct Config : Student
    {
        PPInlineClassTI(Student, Config, ti)
        {
            PPReflAField(ti, map);
        }
        HashMap
<U8String, int> map;
    };
  

下期的主角是非侵入式接口,彻底替换c++上的多继承,功能远远好过C#java杂碎的弱鸡接口,更超越狗语言的不知所谓的非侵入式接口。如果仅仅是完备的反射信息,而缺乏非侵入式接口,在c++下搞面向对象,其实还是很痛苦的。但是,有了非侵入式接口之后,一切豁然开朗。甚至可以说,感觉c++里面搞那么多玩意,都不过是为了给非侵入式接口造势。然而非侵入式接口一直未曾正式诞生过。

posted on 2017-07-11 11:56 华夏之火 阅读(1119) 评论(3)  编辑 收藏 引用 所属分类: c++技术探讨

评论

# re: 再议c++的面向对象能力之上 2017-07-12 09:00 天下

C++标准库如果实现反射+module 秒杀动态语言
C++从11开始发力了.  回复  更多评论   

# re: 再议c++的面向对象能力之上 2017-07-12 13:45 华夏之火

@天下
不要天真,动态语言比c++好多了,c++内核就是一团混乱,只有独特口味的猿猴才会痴迷破烂c++,对这些geek来说,这并不是什么光彩的事情  回复  更多评论   

# re: 再议c++的面向对象能力之上 2017-07-13 10:44 天下

@华夏之火
看你把C++贬的,C++是工业标准,是ISO国际标准,是目前不可缺少的胶水语言...

不像Java和C#是由oracle,ms 这些大公司维护的私有语言。

就是因为C++没有这些大公司商业化的支持和运作,才导致C++的标准库不尽如人意。正因为如此,这个大公司动不了C++,也没法动C++,因为他们说了不算,是C++标准委员会说了才算。

每个语言都是在根据需求而发展、动态进化的,C++也是如此。

无所谓geek不geek。相同的工作经验,C++ 能让猿猴开心,而沉迷其中,比搞Java的,C#多个几K是很普通的情况。
Java,C#都已经烂大街了,一抓一大把,但C++不会。

  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理


导航

统计

常用链接

留言簿(6)

随笔分类

随笔档案

搜索

积分与排名

最新评论

阅读排行榜

评论排行榜