huaxiazhihuo

 

stl的缺陷抽象不足

总的来说,stl整个的设计还是很有水准的,抽象度非常高,采用泛型template手法,回避面向对象里面的虚函数,回避了继承,做到零惩罚,达到了非侵入式的要求(非侵入式远比侵入式要好,当然设计难度也更高出许多)。高性能、可扩展,容器提供迭代器,而算法则作用在迭代器上,容器与算法之间通过迭代器完全解耦,同一种算法可用于多种容器,只要该容器的迭代器满足其算法的要求;而同一个容器,又可被多种算法操作。更重要的是,容器与算法之间完全是开放的,或者说整个stl的体系是开放式,任何新的算法可用于已有的容器,任何新的容器又可被已有的算法所运算。然后,考虑到某些场合下容器的内存分配的高性能要求,分配器allocator也是可以替换,虽然逼格不高。此外,容器算法体系外的边角料,比如智能指针、anyiostream、复数、functio等,也都高性能、类型安全、可扩展,基本上都是c++风格量身定制,这些都无可厚非。真的,stl作为c++的基础库,已经很不错了。只是,依个人观点,当然,以下纯属一家之言,某些情况下,stl可以做得更好,或者说api的使用上,可以更加清爽。以下对stl的非议,似有鸡蛋里挑骨头之嫌,吹毛求疵,强挑毛病。

 

软件框架或者说库的设计,是由需求决定的。脱离需求的设计,不管多精致,代码多漂亮,一切都是废物,都是空谈。程序库所能提供的功能,当然是越强大越好,可扩展,高性能,零惩罚,这些明面上的概念自然重要。殊不知,程序库不能做的事情,或者说,其限制条件,或者说程序库对外界的依赖条件,也很重要。一个基础库,如果它敢什么都做,那就意味着它什么都做不好。事情是这样子的,如果只专注于某件目的,某些条件下的运用,往往可以获得更大的灵活性或者独立性。首先,代码必须很好地完成基本需求,在此基础上,才有资格谈论点什么别的更高层次的概念。

 

上文提到stl由于对动态类型的排斥,所导致的功能残缺以及二进制复用上的尴尬,如果stl愿意正面在动态类型做完善的考虑,相信stl的格局将会大很多,动态类型的话题,本文就不再过多重复了。当然,动态类型的引入必须满足零惩罚的必要条件,c++的程序库,所有不符合零惩罚的概念,最后都将被抛弃。所谓的零惩罚,就是不为用不到的任何特性付出一点点代价。请注意,这里的零惩罚必须是一步都不能妥协。

 

比如说,字符串实现的引用计数、短字符串优化这些奇技淫巧,就不是零惩罚,短字符串优化的惩罚度虽然很轻微,每次访问字符串内容时,都要通过字符串长度来确定其数据的地址,长度小,字符串就放在对象本身上,从而避免动态内存分配。长度大,字符串就放在对象外的堆内存上。短字符串优化空间上的惩罚,字符串对象的占用内存变大了,到了长字符串的时候,字符串对象里因为短字符串的内存空间就没有任何价值。在32位机上,字符串对象可以只包含内存分配器地址,字符缓冲起始地址,字符长度,缓冲的大小,满打满算,也就16个字节。而短字符串优化,就可能要用到32个字节。其实,如果有一个高性能的内存分配器,短字符串优化完全就可以没有任何必要。算了,扯远了,我们还是回到stl的设计思路上吧。

 

大家都知道,stl的字符串其实顶多就是一个字符缓冲管理对象。都98年的标准库了,完全就没有考虑字符编码的需求,真是奇怪之极,令人发指的完全偷工减料。是啊,字符编码不好搞,但是既然有这个需求,就必须支持啊,鸵鸟政策是行不通的。虽然说框架上设计可以既然做不好,那就完全都不做。但是,作为字符串组件,不鸟编码,你真的好意思以字符串自居。撇开编码,string居然有一百多个函数,更让人惊喜的是,这一百多个函数用于日常的开发,还远远不能满足需求。仔细看,这一坨函数大多数仅仅是为了性能需要上的重载,为了避开临时string对象搞出来的累赘。所以,stl里面必须要有一个只读的字符串,不涉及任何动态内存分配,也即是c++17string_viewstring_view里面有一个很良好的性质,string_view的任何一部分还是string_view(不同于c语言字符串的以零结束,必须带零的右部分才是字符串),然后string_view就可以做只读字符串上的运算,比如比较,查找,替换,截取等,分摊string里面大部分的函数。很奇怪的是,string_view这么有用的概念,居然要到c++17里面才有,都不知道stl委员会的人才在想什么。由此可见,如果class里面的成员函数如果过多,好比一百多个,那么其设计思路就一定大有问题,甭管它的出处来自何方。

 

同理可得,只读数组array_view也是很有用的概念,它是内存块的抽象。array_view的任何一部分都是array_view,不同于string_viewarray_view仅仅是长度不能变,但是其元素可修改,可以把array_view看成其他语言的数组,但是array_view不能创建,只能从vector或者c++数组获得,或者又可以看成是切片,array_view本身可以有排序和二分查找的成员函数。Array_view可以取代大多数vector下的使用场合。很奇怪的是,这么强有力地概念,c++17上居然就可以没有。差评!另外,我想说的是,对于排序二分查找,就仅仅用于连续内存块上就好了,其他地方则可免就免,搞那么多飞机干什么,stl在排序二分查找的处理上显然就是过度抽象。

 

或者有人反对,array_viewstring_view不就是两个新的容器,把它们加进stl里,不就行了,stl体系设计完备,绝对对外开放。不是这样的,array_viewstring_view的出现,严重影响现有的stringvector的设计,这两者必须基于array_viewstring_view的存在整个重新设计。

 

Stl就是对良好性质的基础数据结构缺乏抽象,容器的设计只到迭代器为止,提供迭代器之后,就高高兴兴对外宣称完成任务,不再深入地挖掘,可谓固步自封,浅尝辄止。在stl的世界观里面,就只有迭代器,什么都搞成迭代器,什么都只做到迭代器就止步不前,可悲可恨可叹!在其他基础容器的设计上,缺乏深入的考虑,罔顾需求,罔顾用户体验。对于链表的定位,就足以体现其眼光的狭隘。

 

众所周知,单向链表的尾部也是单向链表,可类比haskell或者lisp的列表,这么强有力的好概念,stl里居然完全没有单向链表,更别说凸显此概念。当然,单向链表里面只有一个节点指针,不能包含内存分配器的,也不能有元素计数器,而且生命周期也要由用户控制,但是,用户控制就用户控制,这一点都不要紧,特别是存在arena allocator的情况下,拼命的new单向链表,最后由arena allocator来统一释放内存好了。总之,stl太中规中矩,对于离经叛道的idea,完全就是逃避就是无视,对于动态类型的处理,也是这种态度。Stlallocator的处置,太过简单粗暴,一步错,步步错。

 

而双向链表,在付出O(n)的访问代价后,在为每个元素都要付出前后节点的内存占用后,应该得到咋样的回报呢?显然,stllistO(1)插入,O(n)通过迭代器删除元素,无论如何,完全不能接受,回报太少。首先,O(1)删除元素,不能妥协。为达此目的,我们先开发一个隐式侵入式要求的循环链表,它不关心元素的生命周期,任何插入此链表的元素,其首地址之前必须存在前后节点的指针。然后,链表本身的首两个字段是头节点和尾节点,内存布局上看,循环链表自身就是一个链表节点,一开始,链表为空,其首尾字段都指向自身。这种内存布局下的循环链表和其节点的关系非常松散,节点的插入和删除,只需要修改其前后节点的前后指针的值,完全不需要经过链表本身来完成操作。要删除元素时,只要往前爬两个指针的位置,就得到包含此元素的节点,进而时间O(1)上删除节点,何其方便。显然,循环链表不能包含节点数量,否则每次删除插入节点,都要加11链表的计数器,节点和链表就不能彻底的解耦。这种内存布局上的循环链表,就可支持多态了,比如,比如,xlist<Base> objects,可把Derived1类型的对象d1Derived2类型的对象d2,插入到循环链表xlist里,只要d1d2的前面保留前后节点指针的内存空间。

 

然后,封装这个裸循环链表,用allocator管理其节点元素的生命周期,好像stllist那样,创建删除节点元素。封装过得链表,节点和链表的关系就不再松散,因为节点必须通过链表的allocator来分配内存回收内存。但是,O(1)时间的删除节点,完全是没有任何问题。并且也能支持多态,可插入子类对象。相比之下,可见stllist有多弱鸡,简直不知所谓。

 

不管怎么说都好,stl里面对字符串的支持很薄弱,要有多不方便就有多不方便,虽然比C要好多了,这个必须的。当然,有人会辩解,很多操作很容易就可以添加进去的,但是,如果标准库支持的话,毕竟情况会好很多,不一定要做成成员函数,之所以迟迟未添加这些常用的字符串函数,怀疑是因为找不到合适的方式添加这些操作。字符串的操作可分为两大类,1、生成字符串;2、解析字符串。这两大类,都是格式化大显身手的地方,也即是sprintfscanfc++下的格式化函数,可以是类型安全,缓冲不会溢出,支持容器,支持成员变量名字。这样子,通过格式化,可以吸收大部分的字符串操作。可惜,stl对于反射的排斥,功能强大的格式化处理是不会出现的,而字符串操作,也将永远是stl的永远之痛。堂堂c++唯一的官方标准库,居然对最常用(可以说没有之一)的编程任务字符串操作支持如此灰头土脸,真是要笑死人了。为什么这么说,因为本座的库早就实现了这些想法(包括string_viewarray_view,不带allocator类型参数的各种容器),字符串的处理,简直不要太方便,比之stl的破烂,不可同日而语。比如,在c++中,完全就可以做如下的字符串操作。
vector<byte> buf = {…};
u8string codes;
Fmt(codes, “{ 
~%.2x}”, buf);//codes就是buf的16进制显示,小写,即是”xx xx … xx”。符号~表示前面的部分(这里是一个空格)作为元素之间的分隔符。
vector<byte> copied;
Scanf(codes, “{ 
~%.2x}”, &copied);//这样就把文本codes解析到copied里面去
assert(Equals(buf, copied));

不用格式化,在stl下,用iostream要实现这样的效果,不知道要写多少恶心的代码,反正很多人都已经想吐了。有了格式化之后,日子美好了好多。对了,上面的vector<byte>可换成list<byte>,代码完全都可以成立。Fmt的第一个参数表示格式化结果的目标输出对象,可以是文件,标准输出stdoutgui的文本框等。同时,Scanf的第一个参数表示格式化的输入源,可以是stdin,文件等。总之,FmtScanf这两个函数就可以概括所有的格式化操作,这两个函数,基本上可以解决满足日常上大多数关于字符串的操作。其实格式化的实现,离不开运行时类型信息的支持,本座要有多大的怨念,才会一而再地抱怨stl在反射上的无所作为。

至于iostreamlocale本座就不想批评太多,免得伤心难过,因为iostream竟然出自c++老父bs之手,必须是精品,某种意义上,确实是!


啰里啰嗦一大堆不满,还没有写完。后一篇的文章,主角是迭代器,整个stl的大亮点,同时也是大败笔。既造就了stl框架的灵活性,同时也导致stl函数使用上的不方便,特别是stl算法函数的冗余,非正交,不可组合。你看看,findfind_ifremove, remove_copy, remove_copy_if, remove_if,……,难道就不觉得面目可憎,低逼格,心里难受,堂堂大c++标准库算法的正儿八经的函数,标准会要有多扭曲的审美观,才会这样设计如此丑陋的接口,难道就没有一点点的羞耻心理!这种接口怎么可以拿出来见人见证,丢人现眼。

posted @ 2017-07-09 11:35 华夏之火 阅读(1139) | 评论 (5)编辑 收藏

非完美的stl

       C++类库开发之难,举世公认,最好的证据就是,1983年到现在,面世几十年,就没有一个正儿八经的基础类库。是啊,零惩罚,要高性能,要跨平台,要可扩展,要人性化,又没有垃圾回收的支持,又没有运行时类型信息可用,……,这些方方面面的因素纠结在一起,就好像一个巨大的意大利面线团,真的是众口难调至极。相比C#,java,php等,python等杂碎,它们面世不多久,马上就有官方的标准库,你要说这些杂碎的标准库有多好,那也未必,问题是就有大量人马心悦诚服高高兴兴地用之于开发,没有什么所谓的破心智包袱影响开发效率,甚至有人坚持认为直接用c开发,开发速度都可以快过c++。哪像c++的破事一大坨,总之就是没有一个好的基础库,能够让所有的c++开发者大爷满意。你要说这些c++大爷难侍候,也未必,因为的确就是,不管怎么呕心沥血捣鼓出来的库,确实就是是存在这样那样的问题,以至于后面的大量使用中,缺陷扩大越来越明显,难以忍受。

c++之父一直在重复强调,c++本身美过西施,美得像杨玉环,c++本身没有问题,只是欠缺好用的基础库。问题是好用的基础库千喊万喊,迟迟就是不肯露面。这种情况下,就很让人怀疑c++的存在意义了。因为很明显的事实,其他的后生语言早就有庞大严谨的标准库,就你c++诸多借口,搞不出来合格的基础库,难道不是c++语言本身就存在重大缺陷,所以才有此困境。很多c++的老残党(包括本座),都很赞同c++之父的观点,c++本身很好,就是欠缺好用的基础库。因此大力出奇迹,集整个c++界的精英,花多年的研发,终于奋斗出来stl这个“精品”,另外,还准备了一个候补的boost,以满足非正常性的需求。

平心而论,stl还是相当不错的,高性能,可扩展,零惩罚,跨平台等,基本上都满足要求了。除了二进制不能共用,除了编译速度慢,除了代码膨胀,除了出错的时候,可能铺天盖地的错误,这也是没有办法的事情,世上哪有十全十美之事。总之,在基础设施严重施缺乏的c++上面,能够做出来这么一个玩意,已经很不容易了。最显然的事实,面对着stl,除了一小撮乱党,广大劳动群众普遍都认可stl。只是,既然stl是c++里面如此官方的基础库,就有必要接受更高标准的考验。而最终,stl整个的设计,也不可避免地,也绝非完美。这由此可见,c++基础库开发的难度。

stl里面的字符串,编码,iostream,locale,allocator,algorithm里面算法函数的重复(非正交)等的问题,都只是表象。根子上考察,stl的设计思路上犯了左倾和右倾的问题。具体表现如下:
1、对动态类型的畏惧,对静态类型的过度拥抱。这个问题在c++11之后,有一定程度的改善(出现了shared_ptr, any, variant,内里用到动态类型,起码有virtual的关键字使用)。最明显的表现就是,把内存分配器allocator做成静态类型信息,由此造成的麻烦,真是罄竹难书。同一个整型的vector,因为使用不同类型的allocator,也即是,vector<int, xalloc>和vector<int, yalloc>居然分属不同的类型,然后有一个函数要处理整型数组,要么只能做成模板函数,放在头文件上,c++原本就编译速度龟慢,再这样玩,简直雪上加霜;如果函数坚持放在cpp文件里面,就只能处理专门的allocator的整型vector。基本上,用stl打造的公共代码,都要做成头文件的共享方式,然后每次小小修改,都要引起连锁的编译雪崩,大型的c++项目,对于头文件的修改,考虑到感人的编译速度,从来都是非到不得已的时候,能不动就不动。岂有此理,天理何在。c++17,标准库终于接受多态的allocator,这算是对过去左倾激进的纠正。某种程度可以上改善这个问题,因为到时候就可以只专门接受多态的allocator,只可惜,还不完备。

考虑批量分配arena类型的allocator,理想情况下,对于在此arena allocator上分配的对象,假如仅仅涉及到内存问题,其实大多数情况下,析构函数做的就只是释放内存。那么完全就可以不必苦逼的一个一个调用对象的析构函数,仅仅把arena allocator的内存归还给系统就好了,这对于运行性能的改善,意义重大,本座测过,真是快了很多。问题是,现有stl的体系下,不能保证容器的元素也使用和容器一样的allocator,或者说,容器的allocator对象无法传递给它的元素,让容器元素也使用同一个allocator对象来分配内存。比如说,vector<string>,vector和string的allocator都用polymorphic_allocator,但是,vector的allocator对象和string的allocator可能不是同一个。这样子,我们就不能仅仅简单的归还allocator对象内存,而必须像过去那样子,对vector<string>里面的每一个string都调用析构函数来归还内存了。差评!所以,一开始,allocator就不应该成为模板参数。另外,stl对allocator的粒度也考虑不周。allocator的迥异应用场合起码有几种:1、静态allocator,专门在main函数运行前的使用,用于生成元数据,这些元数据不必一一析构,主函数结束后,统一一次性释放;2、全局的allocator,考虑多线程考虑并发;3、scope,可以在一个任务下使用,任务完毕,统一释放,这里包括函数或者协程;4、gui下的allocator等;只可惜,stl的allocator就只关注全局的allocator。

既然stl对allocator都可以搞成静态类型的鬼样子,那么整个stl对运行时类型信息的忽视,逃避,就可想而知了。typeid得到的type_info,除了起到类型的唯一标识符的作用(动态库下,同一种类型的type_info可能还不一样),并得到类型的名字之外,就不知道这个type_info还有什么鬼用。即便是这么一点小功能,还是能用于很多地方的,比如,any,variant,双分派(double dispatch),由此可见运行时类型信息的重要性。

动态类型信息,也即是反射的重要性,一点都不亚于静态类型信息。意义重大,有了反射,我们就可以将类型当成变量,到处传来传去,也可以保存起来,供后面使用,这里可做的文章,可挖掘的潜力太多了。假如c++的反射信息完善的话,很多头文件上的模板代码实现就可以放到源文件里面,模板函数仅仅是提取一下静态类型的运行时对象,类型擦除,具体实现代码就可以放到cpp代码里面去。然后,虚模板函数也可以成为可能了。可以用来创建对象,析构对象,消息发送,非侵入式的接口,序列化……,甚至,连多继承也都是多余(当然,多继承还是很有用,只是这货不应该出现在正式的场合下)。最典型的例子,格式化printf,通过c++11的variadic template,提取类型的运行时类型对象再连同入参的地址,就可以实现现在c库里面的那个弱鸡sprintf,类型安全,缓冲安全,高性能的效果,不但类型可扩展,连同格式化的控制字符都可扩展,甚至还能支持变量名字。stl里面的iostream、locale的设计成这个鬼样子,也是因为运行时的缺失导致。c++里面要妥当地处理好字符编码、字符串、文件流、locale这几者的关系,绝对不是一件容易的事情,所以也难怪stl在这里的一塌糊涂。看过iostream,locale的实现源码,大家都说不好,大家都很难受,简直可以和mfc媲美,这是真的。

c++的反射可以做到零抽象,也即是,只对必要的类型必要的信息做反射,不像java或者C#,不管是什么类型,不管是信息,一些很明显就是无关紧要的临时的东西,,不管三七二十一,全部一股脑儿都反射起来。甚至,c++的反射,还能添加用户自定义的反射信息,甚至,还能运行时修改反射数据。这里,C#、java等,除了attribute或者注解,就别无他法了。反射的意义就在于,它提供了统一的接口,将类型信息全部集中存放在同一个地方,任何关于类型的运行时信息,全部被标准化公理化。有了完善的反射信息,c++里面做一个eval都手到擒来。说白了,反射就是静态类型语言里把“代码做成数据”的最重要机制(没有之一),虽然比之于lisp的“代码即数据”弱一些,但是已经可以应付99%以上的需求了。甚至可以说,c++的基础库迟迟未出现的原因就是因为反射的缺席而导致的(当然,没有合适的内存管理机制也是重要原因)。而可惜,stl对运行时这一块的关注,不到%1,这真是令人扼腕叹息至极。

2,stl的抽象缺陷:臆造抽象,过度抽象,抽象不足,想当然的抽象,大部分的精力都花在刀背上,或者说是很形式化的学术研究。
突然发现文章已经很长了,就先打住,以后有空再好好发挥。对了,cppblog人气太冷清,门可罗雀。再这样下去,本座只好转战知乎了。

posted @ 2017-07-07 16:52 华夏之火 阅读(1218) | 评论 (6)编辑 收藏

完备的运行时类型信息

众所周知,码猿写代码,自然要求严谨周密,殊不知想象力也很重要。本座阅码几十年,很是感概很多码猿的脑洞被大大禁锢,鲜有人能越雷池一步,特别是c++的同学,连同委员会的那一坨老头子,都很让人无语至极,出自这些人的作品,都是一个死鱼眼睛样子,千人一面,毫无灵动之生趣可言。stl,boost这些库都是这样子(虽然它们确实可以完成大多数日常任务),更别说其他的库,没有什么让人耳目一新之处。

就说说动态类型信息这块,又或者说是反射。自然,语言本身提供的废物type_info就懒得说了,除了证明c++也东施效颦,也能支持动态信息之外,就别无用处了,有谁会正儿八经的用type_info做点正儿八经的事情呢。因此,各路人马纷纷上阵,都要弥补c++在运行时类型信息上的缺失。因为类型的反射信息实在太重要,或者说,反射的用武之地太多太多,表面上很多事情不需要反射,或者字面代码上就看不到反射的痕迹,但是内里的实现,大把大把的反射在发光发热。c++坚持不在动态信息上给予一点点多余的支持,并不表示c++就不需要反射了,看看标准库这个极力回避动多态的典范,是一个怎样的失败作品,嗯,这个以后再谈吧。假如stl一开始就没有如此大力排斥动多态,你看看就连内存分配的allocator都可以做到静态类型信息里面(最新版的c++终于也要接受多态的allocator,c++界居然一片欢呼鼓舞,真是悲哀),今时今日的c++就不会在很多领域上到处割地求和。

总的来说,现在市面上的c++反射库,都是侵入式,都学着mfc那一套,都是要求继承自一个基类Object,然后才能对外提供反射信息的功能,先不说它们提供的类型信息是否完备,这样子就把用途广泛限制死在一个很窄很窄的小圈子里面了。这些反射库,1、不能反射基本类型,int、char、double、const char*、……等;2、不能反射非继承自Object的class或者struct,3、也不能反射模板类,比如vector<int>、list<vector<vector<int>>>。虽然typeid千般弱鸡,但也非一无是处,起码非侵入、平等、多态。所以,理想的反射,应该像c++原生的typeid那样无色无味:1、非侵入式的;2、可以对所有的类型都提供反射,基本类型、非Object系的struct或者class、template类型的;3、多态的,只要改类型需要运行时的类型识别,那么就返回其本身的类型(子类),而非字面上的声明类型;4、支持类型参数,也即是说,以类型传递给该函数时,就返回相应的类型信息对象。

说得具体一点,我们要求的反射库是这样子的。当然,首先要有一个类型信息对象TypeInfo,里面装满了关于对于类型的所有详细信息。如下所示:可以猜到这种反射下框架,只支持单继承,这是故意的。
    struct TypeInfo
    {
    
public:
        template
<typename Args>
        
void ConstructObject(void* obj, MemoryAllocator* alloc, Args&& args)const
        
bool IsDerviedOf(const TypeInfo* base)const;

    
public:
        
virtual TIType GetTIType()const = 0;
        
virtual const InterfaceMap* GetInterfaces()const
        
virtual jushort GetMemorySize()const
        
virtual ConstText GetName() const
        
virtual AString GetFullName()const
        
virtual jushort GetAlignSize() const
        
virtual ConstText GetSpaceName()const;
        
virtual const TypeInfo* GetBaseTypeTI()const;
        
virtual const TypeInfo* GetPointeedTI()const
        
virtual size_t GetHashCode(const void* obj)const;
        
virtual bool IsValueType()const { return true; }
        
virtual bool IsClass()const { return true; }

        
virtual bool DoInitAllocator(void* obj, MemoryAllocator* memAlloc)const;
        
virtual bool NeedDestruct()const { return false; }
        
virtual void DoDefaultConstruct(void* obj)const;
        
virtual bool CanDefaultConstruct()const { return true; }
        
virtual void DoAssign(void* dest, const void* src)const;
        
virtual bool Equals(const void* objA, const void* objB)const;
        
virtual void DoDestruct(void* obj)const;
        
    };
然后,就要有一个函数TypeOf,应该是两个,一个是无参数的类型模板函数,可以这样调用,TypeOf<type>();一个是有一个参数的类型模板函数,可以这样调用,TypeOf(obj)。不管是那一个,其返回结果都是const TypeInfo*。TypeOf的要做到的事情是,对于每一种类型,有且只有一个唯一的TypeInfo对象与之对应,不管是template的还是非template的;比如,以下的几个判断必须成立。
TypeOf<int>() == TypeOf<int>();
TypeOf<int>() == TypeOf(n);    //n为整型
TypeOf<vector<int>>() == TypeOf(nums);//nums的类型为vector<int>
Object* a = new ObjectA; TypeOf(a) == TypeOf<ObjectA>();
其实这里面的原理也没什么神奇,无非就是trait配合sfine,接下来就全部都是苦力活,就是为每一种类型都专门特化一个详细描述的类型对象,用宏可以节省大量的代码。但是整个反射库,本座前前后后重构了十几次,现在也还在重构之中,终究还是解决了开发上所遇到的各种事情。比如,序列化(支持指针、支持多态)、对象与xml的互换、对象与json的互换、数据库表读写对象、格式化、Any类型、非侵入式接口、消息发送、字符串生成对象等等。
其实现方式,概括起来,就是引入间接层元函数TypeInfoImp专门用于返回一个类型type,type里面有一个GetTypeInfo()的函数。然后TypeOf调用TypeInfoImp里的type的GetTypeInfo()最终得到TypeInfo对象。代码如下所示。
    template<typename Ty> struct TypeInfoImp
    {
        typedef Ty type;
        
static const bool value = THasGetTypeInfoMethod<Ty>::value;
    };

    template
<typename Ty>
    
struct TypeInfoImp<const Ty> : public TypeInfoImp<Ty>
    {
        typedef typename TypeInfoImp
<Ty>::type type;
        
static const bool value = TypeInfoImp<Ty>::value;
    };
    
    template
<typename Ty>
    
const TypeInfo* TypeOf()
    {
        typedef typename TypeInfoImp
<Ty>::type TypeInfoProvider;
        
return TypeInfoProvider::GetTypeInfo();
    }
    
    template
<typename Ty>
    
const TypeInfo* TypeOf(const Ty& obj)
    {
        typedef typename IsRttiType
<Ty>::type is_rtti;    //又是间接层,对动态类型和非动态类型分别处理
        return ImpTypeOf(obj, is_rtti());
    }
    
    template
<>
    
struct TypeInfoImp < bool >
    {
        
static const bool value = true;
        typedef TypeInfoImp
<bool> type;
        
static TypeInfo* GetTypeInfo();
    };
        
    TypeInfo
* TypeInfoImp<bool>::GetTypeInfo()
    {
        
static TypeInfo* ti = CreateNativeTypeInfo<bool>("bool");
        
return ti;
    }
可能可以有简洁的方式,比如不需要引入TypeInfoImp,但是实际最终证明TypeInfoImp的方式最具灵活性也最能节省代码。最起码,它在自定义的struct或者class就很方便,只要改struct内部包含一个GetTypeInfo()的函数,它就可以被纳入TypeOf体系中,非常方便。对于模板类型的TypeInfoImp,就要用到哈希表了。比如,对于std::paira的类型信息,如下实现,
    template<typename FstTy, typename SndTy>
    struct TypeInfoImp < std::pair<FstTy, SndTy> >
    {
        static const bool value = true;
        typedef TypeInfoImp < std::pair<FstTy, SndTy> > type;
        static TypeInfo* GetTypeInfo()
        {
            ParamsTypeInfo<FstTy, SndTy> args;
            return PodPair::LookupTemplateTypeInfo(args);
        }
    };
提取其类型参数的const TypeInfo*,生成数组。用此数组到PodPair的哈希表里面查找,如果哈希表中以有此类型数组参数的对象就返回,否则见创建一个添加一条哈希条目,然后返回。每一个泛型类型,比如vector,list,pair都有一个属于自己的哈希表。
打完收工。原理很简单,但是对于工业级的反射库,要考虑很多细节,比如,TypeInfo对象的内存管理;怎么为enum类型生成一堆字符串,以支持字符串和enume值的互相转换;生成并保存class的构造函数和析构函数指针;命名空间的支持;仿真C#里面的attribute;如何以最方便的方式生成成员字段或者成员函数信息等等,一句话,就是他妈的体力活。但是,回报是很丰盛的,这里的苦力活做完之后,程序的其他地方上,基本上,就没有什么重复相似的代码,一切的体力工作全部就可以压在类型信息这里了。

posted @ 2017-07-05 11:45 华夏之火 阅读(1203) | 评论 (1)编辑 收藏

预处理之正整型

      虽然通过一系列的奇技淫巧,让预处理也图灵完备一把,但是用预处理来做计算,真的很吃力不讨好。因为预处理一开始设计出来的目的,就没什么野心,原本就仅仅只是为了做简简单单的文本替换工作,并没有想过要成为正儿八经的编程语言,即便是最最缩水版脚本语言的功能要求都达不到。只是后来,实在是大量要求要批量自动生成代码,特别是c++11之前的版本玩什么模板元编程,铺天盖地的要有大量相似的代码。这些代码用其他工具来生成,当然形式会更加漂亮,但是始终还是用原生的预处理来做这种事情会更加的方便,否则每次修改,都要运行一遍外部工具,都麻烦啊!本人是倾向于用预处理来生成代码的。另外,c++11之后,的确原来很多需要宏来生成代码的场合已经不必要了,但是因为c++11的类型推导能力大大加强了之后,发现又有一大波地方可以用宏来生成代码了。并不是说C++中的宏是必不可少之物,但是用了宏,真的可以减少很多很多的重复代码,起码纸面上的代码清爽了很多。    
          
      预处理的原生数据类型就只有符号,然后符号只支持##的并接运算,同时,预处理也能识别并接后的结果(否则,并接运算就没意义了),如果是宏函数,就进行调用操作,如果是宏符号,就替换文本,如果什么都不是,就什么都不做,保留符号。但是这样的弱鸡类型,显然远远不能满足离经叛道的码猿需要。经过大量的宏编程的尝试之后,可以很肯定一点,预处理里面只能再模拟出来一种数据类型,那就是正整数,虽然通过补码运算来仿真负数,但是由于预处理里面的符号不能包含减号(-)字符,当然要花大力气捣鼓负整数也是可以的,只是使用上也不方便也不直观,性价比不高,基本上,必须用宏来生成代码的地方,都可以不需要负整数的。

     另外,预处理也没有变量类型的概念,不要说强类型,就连弱类型也不是,完全就是无类型。正整数类型的概念全靠码猿人肉编译器来维护,一个循环的宏代码生成一般都是来来回回也不知道调用了多少层宏调用,任何一个地方出错,有时候是几吨密密麻麻的中间失败代码(编译器的预处理缓冲溢出,弃械投降),有时候就完全没有输出,没有任何一丁点的提示,简直是大海捞针的找问题。因此,在用宏循环生成代码时,必须小心翼翼,步步为营,不得不感慨,正儿八经语言里面的类型真是好东西啊。

其实,数据类型并不重要,重要的是数据上能够支持的运算集合以及这些运算能运用的场合。
好了,回到上文,我们用_ZPP_INC_N搞了10个数,通过复制粘贴,可以把N增加到255。实际运用中,完全足够用了。
#define _ZPP_INC_JOIN(_A, _B) _ZPP_INC_JOIN_IMP1(_A, _B)
#define _ZPP_INC_JOIN_IMP1(_A, _B) _ZPP_INC_JOIN_IMP2(~, _A##_B)
#define _ZPP_INC_JOIN_IMP2(p, res) res

#define PP_INC(x, ) _ZPP_INC_JOIN(_ZPP_INC_, x)
#define _ZPP_INC_0         1
#define _ZPP_INC_1         2
#define _ZPP_INC_2         3
#define _ZPP_INC_3         4
#define _ZPP_INC_4         5
#define _ZPP_INC_5         6
#define _ZPP_INC_6         7
#define _ZPP_INC_7         8
#define _ZPP_INC_8         9
#define _ZPP_INC_9         10
...
#define _ZPP_INC_255       256

同样的方式,再如法泡制PP_DEC,从256开始,一直递减到0为止。对于大于256的数,就不支持了,那就都是未定义操作。这样子,通过PP_INC(n),就得到n+1;而PP_DEC(n),则是n-1。比如PP_INC(PP_DEC(9)),其结果肯定是9了。很好,这样子,在预处理中就实现了自然数自增1和自减1的运算了。另外,对于大于256的数,比如512传递给PP_INC,就只得到一个_ZPP_INC_512的符号,完全没有任何意义。

然后,两个自然数是否相等的判断,也非常重要,必须支持。但是,在此之前,要实现一个宏函数PP_NOT,用来判断入参是否为0。为0的话,则函数返回1,否则,就返回0。也即是:
PP_NOT(0) == 1
PP_NOT(23) == 0,或者 PP_NOT(var) == 0。
记住,预处理提供给我们的原生类型就只有符号和##并接运算,除此之外,别无他物。好像工具太简陋,能完成目的吗?不得不佩服有些码猿的脑洞。以下代码是这样运作的,假设PP_NOT生成以下的调用形式,先不管PP_ARG1,至于符号~,是这样子的,可以看成普通的变量名字,它就是占位符,因为预处理只识别逗号(,),和小括号,至于其他符号,完全无视,那些是C/C++编译阶段才关心的符号。
PP_NOT(0) = PP_ARG1(~, 1, 0)
PP_NOT(n) = PP_ARG1(_ZPP_NOT_n, 0)
然后,让PP_ARG1取第二个参数(码猿的计数是从0开始的,也即是,0即是1,1即是2),就完成任务了。至于_ZPP_NOT_n是什么鬼,那个只是中间生成的临时符号,可以舍弃。我们只需对_ZPP_NOT_0做特别处理。因此,代码可以这样写了。PP_PROBE()用以生成两个入参
#define PP_PROBE() ~, 1
#define _ZPP_NOT_0 PP_PROBE()
#define PP_NOT(_X, ...) PP_IS(PP_JOIN(_ZPP_NOT_, _X))
# define PP_IS(...) PP_ARG1(__VA_ARGS__, 0)

这样子之后,显然PP_NOT(n)就可以变成PP_ARG1(_ZPP_NOT_n, 0)的形式了。PP_NOT不是只需一个入参吗?为何后面还要带省略号,纯粹是为了后面各种变态的运用,取悦编译器。已经用宏来写代码了,就不必再遵守什么清规戒律,只要能完成任务就行了。

至于PP_ARG1的实现,就很简单了,如下所示,
#define PP_ARG0(_0, ...) _0
#define PP_ARG1(_0, _1, ...) _1
#define PP_ARG2(_0, _1, _2, ...) _2

然后通过两次取反的函数,再补上函数PP_BOOL,如果入参>0,就返回1,否则返回0,类似于整型到bool的强制类型转换。
#define PP_BOOL(_X, ...) PP_NOT(PP_NOT(_X))

有了这些的铺垫之后,要比较两个自然数是否相等,就简单了。其实没什么神秘的,就是针对从0到255,重复256个以下形式的#define语句,
#define    _ZPP_0_EQUALS_0        PP_PROBE()
#define    _ZPP_1_EQUALS_1        PP_PROBE()
#define    _ZPP_2_EQUALS_2        PP_PROBE()
...
#define PP_EQUALS(x, y) PP_IS(PP_CONCAT4(_ZPP_, x, _EQUALS_, y))
PP_EQUALS就是将入参并接成_ZPP_x_EQUALS_y的形式,只要x和y相同,也即是说,它们在上面的表格中,那么,道理就如同PP_NOT的实现那样,最后结果就是1了。其实,预处理中没有判断这种玩意,只有表格,只有并接,只有查表。所谓的图灵完备,说白了,没有玄虚的,就是建表,然后查表。对相等比较取反PP_NOT,自然就得到不相等的判断函数。
#define PP_UN_EQUALS(x, y) PP_NOT(PP_IS(PP_CONCAT4(_ZPP_, x, _EQUALS_, y)))
再次建表,就可以得到bool运算的函数,或与
#define PP_OR(a,b) PP_CONCAT3(_ZPP_OR_, a, b)
#define _ZPP_OR_00 0
#define _ZPP_OR_01 1
#define _ZPP_OR_10 1
#define _ZPP_OR_11 1

#define PP_AND(a,b) PP_CONCAT3(_ZPP_AND_, a, b)
#define _ZPP_AND_00 0
#define _ZPP_AND_01 0
#define _ZPP_AND_10 0
#define _ZPP_AND_11 1

再准备一张表格,将字节映射到8个二进制位。
#define _ZPP_BINARY_0    (0, 0, 0, 0, 0, 0, 0, 0)
#define _ZPP_BINARY_1    (0, 0, 0, 0, 0, 0, 0, 1)
#define _ZPP_BINARY_2    (0, 0, 0, 0, 0, 0, 1, 0)
#define _ZPP_BINARY_3    (0, 0, 0, 0, 0, 0, 1, 1)
#define _ZPP_BINARY_4    (0, 0, 0, 0, 0, 1, 0, 0)
...
然后通过模拟计算机组成原理里面的加减乘除的原理,就可以实现四则运算了。对了,整个预处理库的代码都在压缩包上,功能比boost的预处理库强多了,但是代码却少了很多,也容易理解多了,所有代码在vs下面正常运行,其他平台还没有测试。代码包:/Files/huaxiazhihuo/preprocessor.rar

posted @ 2017-07-04 14:21 华夏之火 阅读(755) | 评论 (0)编辑 收藏

预处理的图灵完备之引言

好久没有光顾cppblog了,现在这里这么冷清了,不免让人有些伤感,可见c++现在多么的不得人心,也可能是c++的大神去了其他的网络平台,好比知乎。不管怎么样,始终对c++还是有些感情,也对cppblog有些感情。

我们还是来讨论c++吧,这几年在c++里面玩代码自动生成技术,而预处理是不可避免,也是不可或缺的重要工具。虽然boost pp预处理库在宏的运用上很是完善,但是代码也太多了,而且代码很不好理解,对此,不免让人疑惑,有必要搞得那么复杂,搞那么多代码吗?并且,看了boostpp的使用接口后,感觉写得很不干净,也不好组合。因此,重新做了一套预处理的轮子。以下的代码,假设在msvc2013以上的版本运行,反正很多人用MSVC的,装逼的自当别论,造出来的轮子,倾向于先支持msvc。

首先,我们定义一个宏,用来给把入参变成字符串,咦,这个事情也太easy了,但是,在此,感觉,还是有必要废话多解释一下。以下代码惯例都是,所有可用的宏函数都是以PP开头全部大写,而以_ZPP开头的全部都是内部实现,其实还可以做得更难看一点。因为宏函数是全局的,没有作用域的概念,并且只是单纯的文本替换,死的时候,还不知道怎么死,所以,必须谨慎对待。像是windows.h头文件那样,直接用min,max作为宏的名字,虽然用起来很方便,但也不知道制造了多少麻烦,所以,很多时候,包含windows.h时,第一件事情就是undef min和max。

以下的代码,可以随便在某个工程下,随便建立一个cpp后缀名的源文件,然后按CTRL+F7编译,不需要F5,就可以看到运行的效果,如果编译通过,就说明宏基本上正确,测试代码越多,准确性就越高。当然,你们也可以通过设置源文件的属性,让msvc生成预处理后的文件,然后用记事本打开那个文件观看。
#define PP_TEXT(str) _ZPP_TEXT(str)
#define _ZPP_TEXT(str) #str
在c++预处理宏中,操作符#是将后面跟随的表达式加上两个双引号,也就是字符串。PP_TEXT(str)不是直接定义成#str,而是通过调用_ZPP_TEXT(str),然后在那里才将入参变成字符串,显得有点辗转,有点多此一举,但,其实是为了支持宏的全方位展开,也就是入参str本身也存在宏调用的时候,纯属无奈。比如,如果这样实现
#define PP_TEXT(str) #str
那么,对于下面的情况,
#define AAA aaa
PP_TEXT(AAA),结果将是"AAA",而不是"aaa"。因为宏操作符直接是将入参变成字符串,没有让入参有一点点回旋的空间,所以只好引入间接层,让入参有机会宏展开。后面,很多宏函数都是这样实现,不得不间接调用,以便让宏全面展开。而msvc的宏展开机制更加奇葩,更加不人性化,其间接调用的形式也更丑陋。这都是没办法的事情。
然后,为了调试宏,或者测试宏,当然,很多时候,调试宏,还是要打开预处理的文件来对比分析。我们对 static_assert作一点点包装,因为static_assert需要两个参数,c++11后面的c++版本中,static_assert好像只需要一个入参,那时就不需要这个包装了。
#define PP_ASSERT() static_assert((__VA_ARGS__), PP_TEXT(__VA_ARGS__));
PP_ASSERT(...)里面的三个点,是不定参数的宏,而__VA_ARGS__就代表了...所匹配的所有参数,这条语法很重要,要熟练。这里,就不详细解释其用法了,后面会有大把大把的宏函数用到__VA_ARGS__。
好了,我们可以开始用PP_ASSERT(...)做测试了。
PP_ASSERT(2+3==5)
如果,然后编译这个文件,发现编译通过了,比如
PP_ASSERT(2+3==4)
编译的时候,就会报错误信息,error C2338: 2+3==4
好了,测试准备建立起来,就可以开始肆无忌惮的写代码了。一步一步地构建c预处理宏的图灵完备。
显然,当务之急,最根本的宏就是将两个宏参数的并接,也即是##运算符,显然好比#运算那样子,必须给里面参数有宏展开的机会,因此要间接调用,下面是其实现
#define PP_JOIN(_A, _B) _ZPP_JOIN_I(_A, _B)
#define _ZPP_JOIN_I(_A, _B) _ZPP_JOIN_II(~, _A##_B)
#define _ZPP_JOIN_II(p, res) res
竟然不止一层间接,而是两层,又多此一举,是因为发现在做宏递归的时候,一层间接调用还不能让宏充分地展开,所以只好又加间接层,也不明白是何原因,也懒得追究了。现在,接下来,当然是测试PP_JOIN了。各位同学,可以新建立一个测试文件,那个文件include我们的这个宏函数。当然,也可以在同一个文件里面写测试代码,注意分成两段代码,上一段写宏函数,下一段写测试代码,目前来看,都可以的,后面再整理。
PP_ASSERT(PP_JOIN(1+2== 3))
#define A 20
#define B 10
PP_ASSERT(PP_JOIN(A 
+ B, == 30))
有了PP_JOIN,就可以开始做点其他事情了。比如,计数器,
#define _ZPP_INC_JOIN(_A, _B) _ZPP_INC_JOIN_IMP1(_A, _B)
#define _ZPP_INC_JOIN_IMP1(_A, _B) _ZPP_INC_JOIN_IMP2(~, _A##_B)
#define _ZPP_INC_JOIN_IMP2(p, res) res

#define PP_INC(x, ) _ZPP_INC_JOIN(_ZPP_INC_, x)
#define _ZPP_INC_0         1
#define _ZPP_INC_1         2
#define _ZPP_INC_2         3
#define _ZPP_INC_3         4
#define _ZPP_INC_4         5
#define _ZPP_INC_5         6
#define _ZPP_INC_6         7
#define _ZPP_INC_7         8
#define _ZPP_INC_8         9
#define _ZPP_INC_9         10
这里,我们重新又实现了一遍PP_JOIN,这也是没办法的事情,后面在重重嵌套的时候,会出现PP_JOIN里面又包含PP_JOIN的情况,这样会导致宏停止展开了,所以,只好对于每一个要用到JOIN之处,都用自己版本的JOIN。
这是宏函数的实现方式,通过并接,文本替换,一一枚举,才达到这样的效果,也就是说,我们通过JOIN函数,在宏里面构造了一个计数器的数据类型。如果每个宏函数都这样写,岂不是很累。好消息是,只需用这种苦逼方式实现几个最基本的函数,然后通过宏的递归引擎,其他的宏函数就不需这样子一个一个苦逼的并接替换了。
PP_ASSERT(PP_INC(9)==10)
PP_ASSERT(PP_INC(PP_INC(
9)) == 11)
写测试代码习惯了,写起来就很有意思了,测试通过,也是最激动人心的时刻。
接下来,要处理msvc里面宏的恶心行为,然后就结束本引言。
#define PAIR_SECOND(x, y) y
PP_ASSERT(PAIR_SECOND(
1020== 20)
这样子,还不错,下面,再define一个宏函数,让其返回一个pair,也就是两个值
#define MAKE_PAIR(x, y) x, y
然后,这样调用,
PAIR_SECOND(MAKE_PAIR(1020))
编译器马上就不高兴了,warning C4003: “PAIR_SECOND”宏的实参不足
好像是编译器没有先展开MAKE_PAIR(10, 20),然后再调用PAIR_SECOND,而是直接把MAKE_PAIR(10, 20)整个当成一个函数传给PAIR_SECOND,然后,PAIR_SECOND就提示实参不足,然后,硬要测试,
PP_ASSERT(PAIR_SECOND(MAKE_PAIR(1020)) == 20)
显然,无论如何,编译器势必就龙颜大怒了。对此,我们只好再引入间接层,想办法让MAKE_PAIR(10, 20)先展开,然后再传给PAIR_SECOND。这样,就不能直接用这样的形式了,PAIR_SECOND(MAKE_PAIR(10, 20)) 。只好改成这样,下面的几行代码,很有点惊天地泣鬼神的味道。
#define _ZPP_INVOKE_JOIN(_A, _B) _ZPP_IMP_INVOKE_JOIN_I(_A, _B)
#define _ZPP_IMP_INVOKE_JOIN_I(_A, _B) _ZPP_IMP_INVOKE_JOIN_II(~, _A##_B)
#define _ZPP_IMP_INVOKE_JOIN_II(p, res) res

#define PP_INVOKE(m, args, ) _ZPP_INVOKE_JOIN(m, args)
前面几行代码都是PP_INVOKE的JOIN函数实现,可以直接当它们是JOIN函数,关键是PP_INVOKE(m, args, ...)这里,第一个参数m是宏函数,第二个是args,是要传给第一个参数m的参数列表,用括号括起来,至于后面的省略号,是有些时候为了取悦编译器而添加的,也不知道是什么原因,反正这样子就可以了,懒得追究。垃圾宏,垃圾预处理,只要能完成功能就行了,c++中,代码生成代码,重头戏在tmp那里,宏只是小小必要的辅助工具而已。然后,这样调用,
PP_ASSERT(PP_INVOKE(PAIR_SECOND, (MAKE_PAIR(10, 20))) == 20)
编译通过了,好不容易啊!

posted @ 2017-01-14 15:01 华夏之火 阅读(1392) | 评论 (0)编辑 收藏

lisp的括号

       lisp(当然也包括scheme)的元编程(也即是宏)威力非常强悍,相比之下,c++的元编程(template+预处理)简直就是弱爆了,被人家甩几条街都不止。 当然,template的类型推导很厉害,也能生成很多签名类似的class和function,比其他语言的泛型强多了,但是,template再厉害,也不能生成名字相似的function还有变量。 预处理可以生成名字相似的变量和函数。但是,预处理的图灵完备是没有类型这个概念,只有字符串,整数那个东西还要靠字符串的并接来实现。所以,预处理没法得到template里面的类型信息。新版本的c++中有了decltype之后,宏可以通过某种方式以统一的形式来利用类型信息。但是,在代码生成方面,预处理还是很弱智,主要的问题在于宏对于自己要生成的代码结构很难构建语法树,也不能利用编译阶段的功能,比如调用编译阶段的函数。 想说的是,很难以在代码中只用宏来写一个稍微复杂的程序,即使做得到,也要吐好几口老血,还煞难调试。
       lisp就不一样了,宏和语言融为一体,以至于代码即是数据。只要你愿意,完全可以在lisp中只用宏写代码,只要愿意,分钟钟可以用lisp写一个dsl,比如loop就是一个专门处理循环的dsl。甚至,用lisp宏还可以做静态类型推导的事情,也非难事。因此,用lisp宏搞基于对象 (adt)也都有可能,从而优雅的使用.操作符。比如(+ obj1.item obj2.item) (obj.fun 2 "hello")。你说,lisp宏连.操作符都可以做到,就问你怕不怕。
       但是,宏再厉害,也不能随意地搞底层操作内存。恰好与c++相反,c++搞底层随意操作内存太容易了,但是元编程的能力就远远不如lisp了。
       emacs是最好玩的ide,注意不是最强大,猿猴随时可以写代码增加改变emacs的功能,马上见效,不需要任何配置,不需要重启。因此,elisp也是最好玩的语言了,因为最好玩的ide的脚本语言就是它了,呵呵,主要原因还是elisp是lisp的方言,可以承担lisp的很多构思,当然,完全继承是不行的,不过,已经足够做很多很多的事情了。
       不过,本期的话题是lisp的括号,为什么lisp会有那么多的括号,铺天盖地,很容易,就一堆一堆的括号扰人耳目,以至于lisp代码不好手写,只能忽视括号,依靠缩进。括号表示嵌套,相必之下,c系语言的嵌套就没那么恐怖了。一个程序,顶破天,最深层都不会超过十层,连同名字空间,类声明,函数,再到内部的for,if,大括号,中括号,小括号。
       中缀表达式,这个众所周知了,试比较,1+2-3*4/obj.width,没有任何括号,依靠运算符优先级表示层次关系。并且,猿猴也习惯并本能的解析中缀表达式了,因此,代码看起来一目了然。lisp就很可怜了, (- (+ 1 2) (/ (* 3 4) (obj-width obj))),这里面多了多少括号,在转换成这行简单算式的时候,还是在emacs下面写出来的。关键是,虽然前缀表达式没有任何运算级别上的歧义,但是,人眼还是比较习惯中缀表达式了。君不见haskell的括号更少了,其对中缀表达式和符号的运用更深入。关键是,中缀表达式很容易手写啊。易写,自然也表示易读。C#的linq的深受欢迎也因为其好读,无须在大脑里面建立什么堆栈,linq表达式就是上一个处理的结果通过.操作符传递到下一个运算中,非常顺畅,不必返回前面去看看当前的操作数的运算是什么,因为运算符就在眼前了。中缀表达式.操作符,更是灭掉括号的大杀器,比如,obj.child1.child2.value,这里用lisp来搞,4个括号避免不了的。
       试试将java万物皆是对象推向极致,然后没有中缀表达式,1.plus(2).minus(3.mult(4).obj.width)),比lisp要好一些,但也有很多括号了,并且,在minus这里,其括号嵌套也只是减少了一层而已。当表达式复杂起来的时候,这种缺点也要相应的放大。
       变量的就地定义,好像c系的变量要用到的时候才定义这种语法很稀疏平常,没什么了不起的。但是,到了lisp下面时,就知道这是多么贴近人心的便利啊。每次用到新变量,都要引入let表达式,又或许跑到前面的let语句中写变量,要么就打断当前的代码编写,要么就引入新的一层嵌套关系。一个状态复杂的函数,很容易就出现好多个let语法块。而c系的变量就地定义,显得那么淡定。
       return,continue,break等语句就可以把后面的语句拉起来一个层次,假设没有这些关键字,要用if else语句,那么,这些return,continue,break后面的语句都表示要被包含在一个else的大括号中。
       lisp里面的特有语句,with-*等宏,都要求嵌套。几个with-*宏串起来,几个括号嵌套关系就跑不了啦。而c++通过析构函数就多么地让人爱不释手了,java也可以别扭的用finally来应付了。
       控制结构的并行。像是if,for,while或者是class还有函数定义等语句,其后面的代码块是并列在关键字的后面,这样就少了一层嵌套。不过这个作用并没有那么巨大。主要还是前面4点。
       这样,就可以模拟其他语言的特性来灭掉lisp的括号。当然是要到宏了,loop就是一种尝试。但是,下面将走得更远。其实,就是设计一套新的语法了。
       假设这个宏的名字是$block,那么后面的文章就可以这样做。
       1 加入一个$操作符。$表示后面的代码都被收入进去。比如,1+2-3*4/4,就可以写成($block (-) $ (+ 1 2) (/) $ (* 3 4) 4)。于是,with-*等宏的嵌套就可以用$来代替了。虽然,$的作用好像有些欠缺,功能不完备,但是,只要考虑到括号都是在最外层体现的,那么,$就显得很有作用了。
       2 加入let的操作符,表示就地引入变量,其实也即是将变量名字加入上层的(let)的变量列表中,然后在这里插入一条(setq var vlaue)的语句。
       3 加入if,elif,else,for,switch等语句,于是后面的代码块就与之平行了,并且准备一个(let)的语句,用于给with语句添加变量。可以借鉴loop宏的方式
       4 return,break,continue等相应的实现。
       5 支持.操作符,所有关于.的操作,都转换成相应的函数操作,好像以前的cfront在对于成员函数的支持那样子。这里就要有静态类型推导了,可以通过with语句中加入变量的类型说明,给函数添加返回类型的标签。有了这些信息,就可以找到obj相应的成员函数的名字,就可以生成对应的函数调用的form了,这个做起来有点难度。
       ......
       以上,除了第5点,其他都可以借鉴loop的代码来实现。$block里面的代码,便于手写,括号也没有那么面目可憎了。

posted @ 2016-05-20 11:17 华夏之火 阅读(2903) | 评论 (0)编辑 收藏

迭代器的抽象

      迭代器是好东西,也是猿猴工具箱里面的七种武器之一。代码中必然要操作一堆数据,因此就要有容器,有了容器,自然就不可缺少迭代器,没有迭代器,容器使用上就会非常不方便,并且还必须暴露其内部实现方式。比如,在可怜的C语言里面,操作数组容器就要通过整数索引来访问其元素,操作链表容器,就要通过while(node->next!=null)这样的方式来访问其元素。某种意义上讲,整数索引,node->next这些都是迭代器,只是它们的使用方式没有统一起来而已。既然如此,全世界的迭代器的使用方式都统一起来,那么这就是迭代器了。
      基本上,现代化的语言,都会在语言层面上提供foreach之类的语法糖,其形式不外乎是,foreach(e:c){}。就是这样,只要提供元素的名字和容器对象。后面跟着循环体。其思想就是从容器里面取出一个元素,用循环体对这个元素进行操作。循环完毕,就完成了对容器里面数据的操作。这种语法形式,简洁得不能再简洁了。很好很方便,什么额外重复的代码都不劳费心了,甚至连类型推导不用做。真的,类型可以推导的时候,就让编译器推导好了。代码里面必须大规模的使用auto,var这样的关键字。不要担心看不出来变量的类型。变量类型应该从变量的名字中体现出来其抽象意义,当然,不要搞什么匈牙利命名法,那个太丑陋了。
      既然语法糖提供了这种对迭代器的支持操作语法,自然而然,只要涉及到一堆数据这样的概念,不必局限于具体的容器(数组,链表,哈希表),文件夹也是一堆数据,Composition模式也是一堆数据,数列,……,等等所有这些,全部都是概念上的一堆数据,只要提供了迭代器,猿猴就可以很优雅的用foreach这样的语法糖来统一操作数据,多么方便,多么的多态。不管这一堆数据的内部实现方式是什么,后期怎么修改,在foreach这里代码全部都不会受影响。更何况,对于迭代器,语法上不仅仅提供foreach的便利得无以复加的甜糖,还有一大堆的标准库函数来让猿猴操作迭代器,什么排序,查找,映射……。更令人发指的是,C#把迭代器捣鼓得好用得让人伤心难过悲愤欲绝,而linq语法上还可以把IEnumbale整成monad,可以用来作什么cps的变换。迭代器在手,天下我有。
      迭代器这个概念的抽象似乎很理所当然,但是不然,比如,刘未鹏举过Extended STL的例子,操作文件夹。C和C++代码对比。
// in C
DIR*  dir = opendir(".");
if(NULL != dir)
{
  
struct dirent*  de;
  
for(; NULL != (de = readdir(dir)); )
  {
    
struct stat st;
    
if0 == stat(de->d_name, &st) &&
        S_IFREG 
== (st.st_mode & S_IFMT))
    {
      remove(de
->d_name);
    }
  }
  closedir(dir);
}
 
// in C++
readdir_sequence entries(".", readdir_sequence::files); 
std::for_each(entries.begin(), entries.end(), ::remove);
显然,前者是没有迭代器的抽象,后者是有迭代器抽象的简洁异常的代码。第一次看到,惊为天人,其实本就该如此,只是C将这一切搞复杂了。当然,还有一批C 粉反对,说什么代码不透明了,隐藏了代码背后可能的复杂实现。对于这一簇人的坚持不懈反对抽象的态度,真不知该说什么好呢?代码的能力里面,最最重要的事情就是抽 象,通过抽象,猿猴才可以避开细节,将精力集中于更加重要更加复杂的事情。通过抽象,可以减少重复的代码,可以提高类型安全。C++是唯一能在玩抽象概念的同时,又可以兼顾到底层细节的处理,从而不仅能写出高效代码,还能玩出更炫的技巧。很多时候,必须底层玩得越深,抽象的触角才能伸得越高。
      其实,迭代器不必依存于容器。而是,先有了迭代器,才会有容器。请谨记,迭代器可以独立存在。begin和end就代表了一堆数据的概念。至于这一堆数据是如何存放的,这一切都无关紧要。基于此,有必要用class来表达一堆数据这么一个通用性极高的概念。其实,boost里面好像也有这么一个东西。就叫做DataRange吧。为何不叫Range,因为Range另有更重要用途,这么好的名字就是用来生成DataRange,代码不会直接看到DataRange,都是通过Range来生成DataRange。
template<typename Iterator>
struct DataRange
{
    typedef Iterator IteratorType;

    DataRange(IteratorType beginIter, IteratorType endIter) : mBegin(beginIter), mEnd(endIter)
    {
    }

    IteratorType begin()
const { return mBegin; }
    IteratorType end()
const { return mEnd; }
    IteratorType mBegin;
    IteratorType mEnd;
};
      然后,随便搞两行代码试试
   vector<int> vvv = { 1, 2, 3 };
    for (auto i : Range(vvv))
    {
        cout << i << endl;
    }
      其实,C++11概念上就支持一堆数据的操作,只要一个类型struct或者class里面有begin()和end()这一对活宝,并且这一对活宝的返回类型是迭代器,那么就可以尽情的享用foreach的甜糖。那么,何谓迭代器。就是支持三种操作的数据类型:!=(判断相等,用来结束迭代操作),前++(用来到迭代到下一个元素),*(取值)。那么,这就是迭代器了,显然,指针就是原生的迭代器。虽然,整形int也可以++,!=,但是不支持取值操作,所以int不是迭代器。下面就要把int变成迭代器。
template<typename Ty, typename Step>
struct ValueIncrementIterator
{
    typedef ValueIncrementIterator ThisType;
    typedef Ty ValueType;
    typedef Step StepType;

    ThisType(ValueType val, StepType step)
        :mValue(val), mStep(step){}

    
bool operator != (const ThisType& other) const
    {
        
return mValue < other.mValue;
    }

    ValueType 
operator* () const
    {
        
return mValue;
    }

    ThisType
& operator++ ()
    {
        mValue 
+= mStep;
        
return *this;
    }

    ValueType mValue;
    StepType mStep;
};
然后,再用一个函数FromTo(也不知叫什么名字更好),用来生成DataRange。请注意,我们的迭代器怎么实现,那都是细节。最后展示在用户层代码都是干干净净的function生成的DataRange,甚至连尖括号都不见了。也不用写具体是什么类型的DataRange,只须用auto让编译器自动推导类型就好了。
// step = 1是偷懒做法,万一Step的构造函数不能以1为参数就弱鸡了。比如DateTime和TimeSpan
template<typename Ty, typename Step>
auto FromTo(Ty from, Ty to, Step step 
= 1-> DataRange<ValueIncrementIterator<Ty, Step>>
{
    typedef ValueIncrementIterator
<Ty, Step> ValueType;
    
return DataRange<ValueType>(ValueType(from, step), ValueType(to, step));
}
于是,FromTo(1, 10, 2)就表示10以内的所有奇数,可以用for range的语法糖打印出来。
      这里的FromTo是按照上升状态产生一系列数据,同样,也可以产生下降的一堆数据FromDownTo,如果愿意的话,同学们也可以用迭代器形式生成斐波那契数列。不知注意到了,请用抽象的角度理解++和*这两个操作符。++就是为新的数据做准备进入到下一个状态,根据情况,可以有不同方式,进入到下一个状态,比如上面的ValueIncrementIterator根据步长递增到新的数值,ValueDecrementIterator的++却是在做减法,甚至还可以做Filter操作;*就是取到数据,我们可以在*的时候,才生成一个新的数据,这里从某种意义上来讲,其实就是延迟求值;而!=判断结束条件的方式又多种多样。总之,凭着这三个抽象操作,花样百出,基本上已经能够覆盖所有的需求了。
      为了体现这种抽象的威力,让我们给DataRange增加一个函数Concate,用于将两堆数据串联成一堆数据。首先,定义一个游走于两堆数据的迭代器,当它走完第一堆数据,就进入第二堆数据。
//不知道有什么语法能推导迭代器的值类型,所以搞这个辅助函数。可能写成type_trait形式更好,就算偷懒吧
template<typename Iter>
auto GetIteratorValueType(Iter
* ptr) -> decltype(**ptr)
{
    
return **ptr;
}

template
<typename Iter1, typename Iter2>
struct ConcateIterator
{
    typedef ConcateIterator ThisType;
    typedef Iter1 Iter1Type;
    typedef Iter2 Iter2Type;
    
//typedef decltype(*mBegin1) ValueType;
    typedef decltype(GetIteratorValueType((Iter1Type*)nullptr)) ValueType;

    ThisType(Iter1Type begin1, Iter1Type end1, Iter2Type begin2)
        :mBegin1(begin1), mEnd1(end1), mBegin2(begin2), mInBegin2(
false){}

    ThisType(Iter1Type end1, Iter2Type begin2)    
//这里有些蹊跷,不过也没什么
        :mBegin1(end1), mEnd1(end1), mBegin2(begin2), mInBegin2(true){}

    
bool operator != (const ThisType& other) const
    {
        
if (!mInBegin2 && other.mInBegin2)
            
return true;
        
if (!mInBegin2 && !other.mInBegin2 && mBegin1 != other.mBegin1)
            
return true;
        
if (mInBegin2 && other.mInBegin2 && mBegin2 != other.mBegin2)
            
return true;
        
return false;
    }

    ValueType 
operator* () const
    {
        
return mInBegin2 ? (*mBegin2) : (*mBegin1);
    }

    ThisType
& operator++ ()
    {
        
if (mInBegin2)
        {
            
++mBegin2;
        }
        
else
        {
            
if (mBegin1 != mEnd1)
                
++mBegin1;
            
if (!(mBegin1 != mEnd1))
                mInBegin2 
= true;
        }
        
return *this;
    }

    Iter1Type mBegin1;
    Iter2Type mBegin2;
    Iter1Type mEnd1;
    
bool mInBegin2;
};

有了ConcateIterator,DataRange的Concate函数就很好办了。
    template<typename OtherRange>
    auto Concate(
const OtherRange& otherRange)
        
->DataRange<ConcateIterator<IteratorType, decltype(otherRange.begin())>>
    {
        typedef ConcateIterator 
< IteratorType, decltype(otherRange.begin())> ResultIter;
        
return DataRange<ResultIter>(
            ResultIter(mBegin, mEnd, otherRange.begin()), ResultIter(mEnd, otherRange.end()));
    }
然后,试试
    list<int> numList = { 10, 11, 12 };
    for (auto i : Range(vvv).Concate(FromTo(4, 10, 2)).Concate(numList))   //后面随便接容器
    {
        cout << i << endl;
    }
     这样,就把两堆数据串联在一块了,是不是很酷呢?用C++11写代码,很有行云流水的快感,又有函数式编程的风格。下期节目继续发挥,给DataRange加入Filter,Map,Replace等操作,都是将一个DataRange变换成另一个DataRange的操作,显然,这是一种组合子的设计方式,也是吸收了haskell和linq的设计思路。某种意义上讲,就是给迭代器设计一套dsl,通过.操作符自由组合其成员函数,达到用起来很爽的效果,目标就是仅仅通过几个正交成员函数的随意组合,可以在大多数情况下代替stl算法的鬼麻烦的写法。这种dsl的最大好处类似于linq,先处理的步骤写在最前面,避开了函数调用的层次毛病,最外层的函数反而写在顶层。其实迭代器这个话题要展开来说的话,很有不少内容,比如用stackless协程来伪装成迭代器,Foldl,Foldl1,Scan等。当然,真要用得爽,还要配合boost中lambda的语法,好比什么_1+30,_1%2,当然,那个也可以自己写,因为C++现在已经支持lambda了,所以,自己写boost lambda的时候,可以剪裁,取其精华,去其糟粕。如果,再弄一个支持arena内存批量释放又或者是Stack风格的allocator(线程相关),那么就更不会有任何心智负担了,内存的分配和释放飞快,这样的动多态的allocator写起来也很有意思,它可以根据不同情况表现不同行为,比如说多线程下,就会用到线程同步,单线程就无须同步,每个线程单独拥有一个allocator,根据用户需要,还能用栈式内存分配,也就是分配内存时只是修改指针而已,释放时就什么都不做了,最后通过析构函数,将此allocator的内存一次性释放。当拥有一个表现如此多样的allocator,stl用起来真是爽。

posted @ 2016-05-14 02:10 华夏之火 阅读(1303) | 评论 (2)编辑 收藏

c++单元测试框架关键点记录成员函数地址

原则上,C++下最好的单元测试代码应该长成这样子,用起来才是最方便的
TEST_CLASS(className)
{
    
// 变量
    TEST_METHOD(fn1)
    {
        
// 
    }    
    TEST_METHOD(fn1)
    {
        
// 
    }
    
//
}
vczh大神的测试代码是这样子,这是最方便使用的形式,但因为是以测试方法为粒度,大括号里面就是一个函数体,所以显得功能上有些不足。
TEST_CASE(ThisIsATestCase)
{
TEST_ASSERT(1+1==2);
}
      当然,这里隐藏了很多宏的丑陋实现,但是,那又有什么要紧呢。好不好并不是在于用了什么东西,goto,多继承,宏,隐式类型转换,……,这些,如果能够显著地减少重复性相似性代码,还能带来类型安全,然后又其潜在的问题又在可控的范围之内,那么,又有什么理由拒绝呢。老朽一向认为,语言提供的语法糖功能要多多益善,越多越好,当然,必须像C++那样,不用它们的时候,就不会带来任何代价,那怕是一点点,就好像它们不存在,并且它们最好能正交互补。但是,你看看,cppunit,gtest的测试代码又是什么货色呢。
      据说cppunit里面用了很多模式,其架构什么的非常巧妙。反正使用起来这么麻烦,要做的重复事情太多了,这里写测试函数,那里注册测试函数,只能表示,慢走不送。gtest据说其架构也大有讲究,值得学习,用起来,也比cppunit方便,但是,看看TEST_F,什么SetUp,TearDown,各种鬼麻烦,谁用谁知道。一句话,我们其实只需要class粒度的测试代码,其他的一切问题就都是小case了。
      当然,class粒度的单元测试实现的难点在于收集要测试的成员函数。这里不能用虚函数。必须类似于mfc里面的消息映射成员函数表。也即是当写下TEST_METHOD(fn1),宏TEST_METHOD就要记录下来fn1的函数指针。后面跟着的一对大括号体是fn1的函数体,已经越出宏的控制范围了,所以只能在前面大做文章。下面是解决这个问题的思路。这个问题在C++03之前的版本,比较棘手。但是,所幸,C++11带来很多逆天的新功能,这个问题做起来就没那么难了。下面的思路省略其他各种次要的细节问题。
首先,我们定义一个空类和要测试的成员函数的形式。
struct EmptyClass{};
typedef void(EmptyClass::*TestMethodPtr)();
还有存放成员函数地址的链表节点
struct MethodNode
{
    MethodNode(MethodNode
*& head, TestMethodPtr method)
    {
        mNext 
= head;
        head 
= this;
        mMethod 
= method;
    }
    MethodNode
* mNext;
    TestMethodPtr mMethod;
};
还有提取成员函数地址的函数

template 
<class OutputClass, class InputClass>
union horrible_union{
    OutputClass 
out;
    InputClass 
in;
};

template 
<class OutputClass, class InputClass>
inline 
void union_cast(OutputClass& outconst InputClass input){
    horrible_union
<OutputClass, InputClass> u;
    static_assert(
sizeof(InputClass) == sizeof(u) && sizeof(InputClass) == sizeof(OutputClass), "out and in should be the same size");
    u.
in = input;
    
out = u.out;
}
template
<typename Ty>
TestMethodPtr GetTestMethod(
void(Ty::*testMethod)())
{
    TestMethodPtr methodPtr;
    union_cast(methodPtr, testMethod);
    
return methodPtr;
}
方法是每定义一个测试函数,在其上面就先定义一个链表节点变量,其构造函数记录测试函数地址,并把自身加入到链表中。但是,在此之前,我们将遭遇到编译器的抵触。比如
struct TestCase
{
    typedef TestCase ThisType;
    MethodNode
* mMethods = nullptr;

    TestMethodPtr mTestMethodfn1 
= GetTestMethod(&fn1);
    void fn1(){}
};
      vc下面,编译器报错 error C2276: “&”: 绑定成员函数表达式上的非法操作
      原来在就地初始化的时候,不能以这种方式获取到地址。然后,试试在TestCase里面的其他函数中,包括静态函数,就可以将取地址符号用到成员函数前面。
      这好像分明是编译器在故意刁难,不过,任何代码上的问题都可以通过引入中间层来予以解决。用内部类。
struct TestCase
{
    typedef TestCase ThisType;
    MethodNode
* mMethods = nullptr;

   
struct Innerfn1 : public MethodNode
    {
        Innerfn1(ThisType
* pThis) : MethodNode(pThis->mMethods, GetTestMethod(&ThisType::fn1))
        {
        }
    } mTestMethodfn1 
= this;
    
void fn1(){}

    
struct Innerfn2 : public MethodNode
    {
        Innerfn2(ThisType
* pThis) : MethodNode(pThis->mMethods, GetTestMethod(&ThisType::fn2))
        {
        }
    } mTestMethodfn2 
= this;
    
void fn2(){}
};
      有多少个测试方法,就动用多少种内部类。然后,一旦定义一个测试类的变量,那么这些内部类的构造函数就执行了,把测试方法串联在一块,逆序,也就是说最后定义测试方法反而跑到前面去了。这样子就自动记录下来所有的测试方法的地址。有了这些函数地址信息,后面怎么玩都可以。包括漂亮的测试结果显示,日志记录,甚至嵌入到vs的单元测试界面中,又或者是生成配置文件,各种花招,怎么方便就怎么玩。这个时候,可以拿来主义,把cppunit,gtest等的优点都吸收过来。
      是否觉得这还不够,好像有很多事情要做。比如说,测试方法逆序了,在同一个测试类的变量上执行这些测试方法,会不会就扰乱类的内部信息了,每次new一个测试类,所有的测试方法都要重复记录,内部类变量要占内存……。咳咳,这些都可以一一解决。这里只是用最简明的方式展示自动记录测试方法,产品级的写法肯定大有讲究了。
      可以看到上面的代码都是有意做成很相似的,这些都是准备给宏大展身手的。这些低级宏太容易编写了,任何经历mfc或者boost代码折磨的猿猴,都完全能够胜任,这就打住了。对了,这里的自动记录成员函数的宏手法,可以大量地使用到其他地方,比如说,自动生成消息映射表,比mfc的那一套要好一百倍,应用范围太广了。当初老朽以为就只能用于单元测试框架的编写上面,想不到其威力如此巨大,消息系统全靠它了。C++的每一项奇技淫巧和功能被发现后,其价值都难以估量,好像bs所说的,他老人家不会给c++增添一项特性,其应用范围一早就可以预料的。对付一个问题,C++有一百种解决方案,当然里面只有几种才最贴切问题领域,但是很多时候,我们往往只选择或者寻找到另外的那90多种,最后注定要悲剧。

posted @ 2016-05-11 18:01 华夏之火 阅读(1519) | 评论 (0)编辑 收藏

消息发送杂谈

      最近在看MFC的代码,虽然这破玩意,老朽已经很熟悉了得不能再熟悉了,但是这些破代码自由其独有的吸引力,不说别的,单单理解起来就非常容易,比之什么boost代码容易看多了,单步调试什么的,都非常方便,堆栈上一查看,层次调用一目了然。一次又一次地虐这些曾经虐过老朽的代码,也是人生快事一件。平心而论,mfc的代码还是写得挺不错的,中规中矩,再加上过去九十年代之初,16位的windows系统,那个时候的面向对象的c++的风靡一时,完全采用标准c++,能做成这样,实属难能可贵,也在情理之内。并且,后面压上com之后,mfc也不崩盘,采用内嵌类实现的com的方式,也很有意思。然后,从mfc中也能学到不少windows gui的使用方式还有各种其他杂七杂八东西,虽然win32已经没落。但是里面的技术还是挺吸引人,可以消遣也不错。当然,对于新人,mfc不建议再碰了,mfc真是没饭吃的意思。你想想,一个gui框架,没有template可用的情况下,而逆天c++11的lambda作为匿名functor,更加不必提了,只有虚函数和继承可用,也没有exception,能搞成mfc这副摸样,的而且确是精品。其实,后来的巨硬也有救赎,看看人家用template做出来的专为com打造的atl又是什么样子呢,然后建构在atl的windows thunk基础上开发的wtl又是怎样的小巧玲珑。巨硬在template上的使用还是很厉害的,atl将template和多继承用的真是漂亮。人家几十年前就将template和多继承用得如此出神入化,反观国内,一大批C with class又或者狗粉一再叫嚣template滚出c++,多继承太复杂了,运算符重载不透明,心智负担,隐式类型转换问题太多,virtual是罪恶之源万恶之首,构造函数析构函数背着马猿做了太多事情,exception对代码冲击太大,打断代码正常流行,时时刻刻都好像隐藏着不定时炸弹。依本座看来,C++中一大批能够显著减少重复代码,带来类型安全的拔高抽象层次的好东西,对于这些C语言卫道士而言,都是混乱之物。其实,c语言就是一块废柴,抽象层次太低,可以做文章的地方太少了。
      就以构造函数和类型转换operator为例,来看看怎么用于C的char *itoa(int value,  char *str,  int radix)。
      itoa的参数之所以还需要str入参,那是因为C语言中缺乏返回数组的语言元素,所以调用者要提供一个字符数组作为缓冲用于存放结果,但是这个多出来str参数真是没必要啊,因为语言能力的欠缺,所以只好把这个负担压到猿猴身上。也有些itoa的实现没有这个累赘str的入参,而是内部static的字符数组,用于存放结果并返回给上层。这个版本就只有两个入参了,但是也太不安全了,别提多线程了。假如,有一个函数fn(char* str1, char* str2),然后,这样调用fn(itoa(num1),itoa(num2)),画面太美了。另外,那个有多余str参数版本的itoa也好不到哪里去,要劳心费神准备两块字符数组,然后还要保证参数传递的时候不要一样。反正C语言的粉丝整天很喜欢写这些重复代码,并且美其名曰掌控一切细节的快感。
请看构造函数和类型转换operator怎么解决。太easy了。

struct ToString
{
    
char text[28];
    
int length;

    ToString(
int n)
    {
        
//转换字符串,结果存放于text中
    }

    
operator const char*()
    {
        
return text;
    }
};
      并且,这里的ToString还可以安全的用之于printf里面呢,因为它本身就是字符串的化身。为什么是ToString,因为它不仅仅要它ToString int,还有double,bool,char,……
      不好意思,扯远了,只是想说,框架或者函数库的表现能力也要取决于语言本身的表达能力。就好像C#就可以做出linq那样爽快的框架,java再怎么拼命也捣鼓不出来一个一半好用的linq,C++也不行,但是C++可以捣鼓类似于haskell中的map,filter,fold等,  并结合linq的后缀表达方式。就好比下面这样
      vector<int> nums = {...}
      Range(nums).Map(_1 * _1).Filter(_1 % 2).CopyTo(dest); // 用了boost中的lambda表达法,因为的确很简洁,没法控制。对于复杂情况,当然要用C++11原生的lambda
      勉勉强强差可满足吧。如果C++的lambda参数可以自动推导就好了,不过也没什么,主要是ide下用得爽。用泛型lambda也能将就。
      所以,回过头来,再看看mfc(没饭吃),就可以了解其各种隐痛了。真的,以90年代的眼光来看,mfc真是做到极致了。mfc不可能走win32下窗口函数C语言那样的消息发送消息反应的正路(邪路)吧。窗口函数这一套,在90年代面向对象盛行的时代,绝对不能被忍受,只是到了前几年,才被发现其价值原来不菲,这是解耦合砍继承树的好手法,老朽在前几年也跟风吹捧窗口函数的那一套。平心而论,smalltalk的这一套消息发送的动态语言,确实是很强有力的抽象手段,我不管目标对象能否反应该消息,闭着眼睛就可以给你发送消息,你能反应就反应,不能反应就拉倒,或者调用缺省的反应方式,就好像DefWindowProc(职责链模式?),又或者是抛出异常,怎么做都可以。一下子就解开了调用者和目标对象的类型耦合关系。面向对象中,消息发送和消息反应才是核心,什么封装继承多态,那是另一套抽象方式,虽然坊间说这也是面向对象的基本要素,但是不是,当然,这或许也只是个人观点。
      或许,从某种意义上讲,C++/java/C#一类的成员函数调用形式,其实也算消息发送吧。比如,str.length(),就是给对象str发送length的消息,然后str收到length消息,作出反应,执行操作,返回里面字符串的长度。靠,这明明就是直接的函数调用,搞什么消息发送的说辞来强辩,颠倒是非黑白,指鹿为马。可不是吗?编译器知道str是字符串类型,知道length成员函数,马上就生成了高效的函数调用方式。在这里,没有任何动态多态的可能,发生就发生了,一经调用,动作立马就行动,没有任何商量的余地。耦合,这里出现强耦合,调用者和str绑在一块了,假如以后出现更高效率更有弹性的string的代替品了,可是没法用在这里了,因为这里str.length()的绑定很紧很紧。
      人家消息发送就不一样了,动态的,可以动态替换对象,替换方法,弹性足足。并且,消息发送的模式下,对象收到消息,要判断消息,解析消息,找到消息的执行函数,最后才终于执行任务。这么多间接层,每一层都可以做很多很多文章。比如,在消息到达对象之前做文章,就可以搞消息队列,把消息和参数暂存起来,这个时候,什么actor模式就大放异彩,至于undo,redo,更加是小菜一碟。然后呢,再给对象安装消息解析器,把消息和消息参数转换成其他类型消息。比如原本对象不能反应这条消息,但是对消息参数稍加修饰,然后在发送给对象,这不就是适配器模式。总之,可操作可挖掘的空间太大了,远远不止23条。
      但是,封装继承多态就一无是处了吗?不是的,最起码一点,编译期间可以报错。因为的确有很多时候,我们明明就知道对象的类型,明明就知道对象不可能是其他类型,比如字符串,比如复数,比如数组这些玩意,无论如何,它们都不需要动态消息的能力。我们就知道手上的对象就是字符串就是复数,不可能是别的,并且,我们就是要明确地调用length函数。我们就是要编译器帮忙检查这里潜在的语法类型错误,比如对复数对象调用length函数,编译器马上就不高兴了。并且,一切都是确定的,所以编译器也能生成高效的代码,高效的不能再高效了。对此,消息发送的面向对象就做不到了,不管是什么对象,int,string,complex种种,都来个消息发送。这样一来,静态类型检查和高效的代码,就木有了。
考察一下,面向对象有等级之分,一步一步,有进化的阶梯。每进化一次,就多了一层间接,类型耦合就降低,就进一步超越编译器的限制,当然,也意味着编译器帮忙检查类型错误生成高效代码就弱了一分。事情往往就是,有所得必有所失。少即是多,多即是少。因此,可推得少即是少,多即是多。少始终是少,多始终是多。
      一切,还是要从C语言说起,C语言中,没有class,没有函数重载。函数名是什么,最后就是什么。在这种条件下,代码多了,每个新的函数名字要考究半天,一不小心,要么函数名字就会很长,要么函数名字短了要冲突或者不好理解。但是好处是,最后生成目标代码时,什么函数名字就是什么名字,所见即所得,没有异常,不会捣鬼,于是其他各种语言都可以高高兴兴开开心心调用。猿猴观码,也很清晰。C++也是在这里赚了第一桶金。其实,这么苛刻的条件下,最考究猿猴的代码架构能力,架构稍微不好,最后都势必提早崩掉,前期就可以过滤很多垃圾架构。
      然后就是C with class了,开始在函数名字上面做文章了。同一个函数名字依对象类型,开始拥有静态多态能力了。比如,str.length(),这是对字符串求长度。数组的变量,nums.length(),对数组求长度。同一个length的名字,用在不同的对象上,就有不同的意义。这如何做到呢,最初,cfront(第一版C++编译器)的处理方式是,可以说是语法糖,就是在名字和调用形式上做文章,比如,str.length(),变成,string_length(&str),array_length(&nums)。别小看这点小把戏语法糖,这真是有力的抽象手法。不说别的,就说起名字吧,可以统一length了,无须费思量string_length,list_length了。然后,对象统一调用方式,str.length(),list.length(),函数确定这种吃力不讨好的事情就交给编译器去做好啦,解放部分脑细胞。这,的确很好,但是,全局函数是开放式的,而对象的成员函数是封闭的,一旦class定义完毕,成员函数的数量也就定死了。猿猴最讲究东西的可扩展性,不管成员函数多么方便多么抽象有力,就扩展性而言,就差了一大截,其他优势都弥补不了。语义上看,扩展成员函数的语法完全与原生的一样,增加一个简单的语法形式来扩充,但是多年下来,标准委员会都不务正业,哎。显然,编译器的类型检查能力和生成的代码性能,没有任何减少,但是,猿猴看代码,不能再所见所得了,必须根据对象类型,才能确定最终的目标函数。就这么点小改进,当时C++马上就展示其惊人的吸引力。假如,C++只能留在这一层,相信到今天为止,可以吸引到更多的c粉。可是,C++开始叛变。
      C++的函数重载,还有操作符重载,外加隐式类型转换和隐式构造函数,还有const,volatile修饰,当然,代码可以写得更加简洁,编译器可以做的事情也更多啦,但是函数的调用再也不明确了。部分专注于底层的猿猴的弱小的抽象能力把控不住了,不少人在这里玩不动了。此外,命名修饰把最终函数名字搞得乱七八糟,二进制的通用性也要开始废了。导致C++的dll不能像C那样到处通吃。像是狗语言就禁止函数重载这个功能。大家好像很非难C++的操作符重载,但是haskell还能自定义新的操作符呢。虽然在这里,编译器还能生成高效代码,但是,各种奇奇怪怪类型转换的规则,编译器也偶尔表现出奇,甚至匪夷所思,虽然一切都在情理之内。
      其实,不考虑什么动态能力,单单是这里的静多态,基于对象(俗称ADT)的抽象模式,就可以应付70%以上的代码了。想想以前没有静多态的C日子是怎么过的。
      此时,开始兵分两路,C++一方面是动多态发展,表现为继承,多继承,虚继承,虚函数,纯虚函数,rtti(废物,半残品),到此为止了,止步不前;另一方面是继续加强静多态,王者,template,一直在加强,模板偏特化,template template,varidiac tempalte,consexpr, auto,concept,……,背负着各种指责在前进,就是在前进。C++企图以静态能力的强悍变态恐怖,不惜榨干静态上的一点点可为空间,累死编译器,罔顾边际效应的越来越少,企图弥补其动态上的种种不足。这也是可行的,毕竟haskell都可以做到。template的话题太庞大了,我们言归正传,面向对象。
      下面就是被指责得太多的C++多继承,虚函数,RTTI,脆弱的虚函数表,等,这些说法,也都很有道理,确是实情,兼之C++没有反射,没有垃圾回收,用上面这些破玩意捣鼓,硬着头皮做设计做框架,本来就先天能力严重不足,还要考虑内存管理这个大敌(循环引用可不是吹的),更有exception在旁虎视眈眈,随时给予致命一击。更要命的是,多继承,虚函数,虚继承,这些本来就杀敌八百自伤一千,严重扰乱class的内存布局,你知道vector里面随随便便插入元素,对于非pod的元素,不仅仅是移动内存,腾出新位置来给新对象安营扎寨,还要一次又一次地对被移动的对象执行析构拷贝构造。没有这些奇奇怪怪的内存布局,vector的实现应该会清爽很多。稍微想想,这实在太考究猿猴的设计能力,其难度不亚于没有任何多态特性的C语言了。可以这么说,继承树一旦出现虚继承这个怪胎,整体架构就有大问题,毫无疑问,iostream也不例外。不过,如果没有那么多的动态要求,好比gui框架的变态需求,严格以接口作为耦合对象,辅以function,也即是委托,又可以应付多起码15%的局面。其实,必须要用到virtual函数的时候,将virtual函数hi起来,那种感觉非常清爽,很多人谈virtual色变,大可不必。C#和java还加上垃圾回收和反射,这个比例可以放大很多。在这种层次下,接口最大的问题是,就好像成员函数,是封闭的。一个class定义完毕,其能支持的interface的数量也就定死了,不能再有任何修改。interface可以说是一个class的对外的开放功能,现实世界中,一种东西的对外功能并不是一开始就定死了的,其功能也在后来慢慢挖掘。但是,C++/java/C#的接口就不是这样,class定义完毕,就没有任何潜力可言了。明明看到某些class的能力可以实现某些接口,甚至函数签名都一样,对不起,谁让你当初不实现这个接口。对此,各种动态语言闪亮登场,或mixing或鸭子类型。接口还有另一尴尬之处,比如,鸟实现了会飞的接口,鸭子企鹅也继承了鸟,自然也就继承了会飞的接口,没办法不继承。面对着一个需要IFlyable参数的函数,我们顺利的传一只企鹅进去,然后企鹅再里面始终飞不起来,就算企鹅在被要求飞的时候,抛出异常,也不过自欺欺人。这种悲剧,就好像有些人很会装逼,最后一定会坏事。搞出接口这种破事,就是为了让编译器做类型检查的。又有人说,bird应当分为两类,会飞的和不会飞的,这的确能解决飞行的尴尬。但是,有很多鸟具备捉虫虫的能力,然后又有那么一小撮鸟不会捉虫只会捉鱼,难道又要依据捉虫能力再划分出鸟类。于是鸟类的继承树越长越高,画面越来越美。这分明就是语言能力的不足,把问题交给猿猴了。请谨记,接口就是一个强有力的契约,既然实现了一个接口,就说明有能力做好相关的事情。再说,既然interface这么重要,于是我们再设计class的时候,就自然而然把精力放在interface这个对外交流媒介的手段之上了,而忽视了class本身的推敲。class最重要的事情就是全心全意做好独立完整最小化的事情,其他什么对外交互不要理会。一个class如果能够完整的封装一个清晰的概念,后面不管怎么重构,都可以保留下来。但是,interface会分散这种设计。接口的悲剧就在于企图顶多以90分的能力去干一百分的事情,并且还以为自己可以做得好,硬上强干,罔顾自身的极限。往往做了90%的工作量,事情恰恰就坏在剩下来的10%上。
      于是,狗语言走上另一条邪路,鸭子类型。只要class,不,是struct,这种独特关键字的品味,只要某个struct能够完全实现某个interface的所有函数,就默认其实现了这个接口。并且,狗语言还禁止了继承,代之以“组合”这个高大上的名词了,但是,细究一下语义和内存布局(忽略虚函数表指针),你妈的,不就是一个没有virtual继承的弱多继承吗?显式的继承消失了,隐式的继承还存在的,好了,还不让你画出继承树关系图,高高兴兴对外宣称没有继承了,没有继承并不表示继承的问题木有存在。但是,因为狗语言的成员函数方法可以定义在class,不,struct外面,其扩展性就非常好了,对于一个interface,有哪些方法,本struct不存在,就地给它定义出来,然后,struct就轻松的实现了该接口,即使原来的struct不支持该接口,以后也有办法让它支持,很好很强大。之所以能做到这一点,那是因为狗语言的虚函数表是动态生成的。小心的使用接口各种名字,部分人应该狗语言用起来会相当愉快。可是,你妈,不同接口的函数名字不能一样啊,或者说,同一个函数的名字不能出现在不同接口中。不过,这个问题并不难,不就是不一样的名字吗,c语言中此等大风大浪猿猴谁没有见识过。对于狗语言,不想做太多评断,只是,其扩展性确实不错,非侵入式的成员函数和非侵入式的接口,理应能更好地应付接口实现这种多态方式,只是,编译器在上面所做的类型约束想必会不如后者,重构什么的,想必不会很方便。自由上去了,约束自然也下来了。听起来挺美,但是内里也有些地方要推敲,反正老朽不喜欢,以后也不大会用上,当然,给money自然会用,给money不搞c++都没问题。老朽还是比较喜欢虚函数的接口,更何况c++通过奇技淫巧也能非侵入式的给class添加接口。在静态语言中搞这种鸭子类型的动态语言接口,显得有点不伦不类。
      然后就是com接口的面向对象,完全舍去编译器对接口类型的约束,自然能换来更大的自由。由于com的语言通用性目标,所以搞得有点复杂,但是com背后的理念也挺纯洁。老朽猜测com好似是要在静态语言上搭建出一个类似于动态语言的运行平台,外加语言通用性。其契约很明确,操作对象前时,必须先查询到对象支持的接口,进而调用接口的函数。这里有意思的地方在于面对着一个com对象,你居然没有办法知道到它究竟实现了多少接口。
      最后就是消息发送了,其能力之强大,谁用谁知道。原则上讲,可以看成对象拥有的虚函数表的方法无穷多,又可以把每一条消息看成一个接口,那么,对象可能就实现了无穷多的接口。你说,面对着这样对象,还有什么做不出来呢。真用上消息发送这种隐藏无数间接层,就没有什么软件问题解决不了的。任何软件问题不就是通过引入间接层来解决的嘛。现在用上消息发送这种怪物,就问你怕不怕。没有免费午餐,自然要付出类型安全的危险和性能上的损失。

posted @ 2016-05-10 22:40 华夏之火 阅读(1648) | 评论 (3)编辑 收藏

挖坑,有空填坑


先挖坑,计划写出一系列文章,探讨将c++用成动态语言,或者函数式语言,以达到快速开发的目的,并且在需要优化的情况下,又能够方便快速的优化。现在事务太多,不知道何时能填坑

宏的图灵完备,用宏生成代码,特别是反射,模式匹配,实在必不可少,以至于宏可以与c++的继承、template、exception等基本组件并列的重要必不可少的补充手段

最小巧方便使用的单元测试框架,比gtest,cppunit要好用很多

自定义内存管理器,stl中的allocator是作为模板参数来传递,尝试以tls来传递allocator参数,当然,必须相应的各种容器都要重写,修改其缺省构造函数,拷贝复制移动拷贝,给元素分配内存释放内存等。对了,还有各种容器的反射信息。每种类型的template的容器都有一个typeinfo对象,具体的容器又有自己独一的typeinfo对象

完善完备的reflection,也就是,其他language能够做的反射的事情,这里只要愿意,也可以做到,非侵入式,可以给int,double等基本类型添加反射,给template类型的也添加反射信息,保证每种类型的反射对象是唯一的;

史上功能最完善的fmt的实现,非template,当然,外层还需要variadic来包装,以类型信息。类型安全,缓冲安全,高效,通用。通用的意思是,可以fmt到文件,日志,字符串,文本框控件中;类型安全的意思是,可以是所有的类型都可以fmt,只要该类型实现了相应的接口,但是,这种接口是非侵入式的,通过模板特化。高效的意思是合sprintf系列一样。调用的时候如下:
fmt(text, "%s %s %d ", 20, 17.5, 'a'); //故意写错%s的,在这里,%s为通用符号
fmt(file, "{%s-}",{1, 2, 3}); //输出 1-2-3到文件中,也即是能够fmt容器对象,横线-为容器对元素的分隔符

带有切片功能的数组,此数组类型还支持子类型数组到基类型数组的隐式转换,也即是需要用到基类型数组的参数,子类型数组都可以适应

haskell的map,filter,fold算法在C++下的方便灵活组合性的改造,使用时,就好像C#的linq那么爽快,当然,没有lambda的参数自动推导,毕竟还不如

stackless协程

c++下的monad

wpf的依赖属性在c++下的实现,gui框架的不可缺少的要素

tupple的功能扩展,通过宏,不需要写类型,用起来就好像函数式语言原生的那么爽的可能

好像haskell或者f#那样的模式匹配的结构体

C++下完完全全实现狗语言的那种鸭子类型的接口

面向对象的深入探讨,对于企鹅或者鸡是一种鸟,继承了鸟,但是没有继承了会飞的接口,在编译期就能报错,在运行期也不能对其找到会飞的接口

具体类,基本类型,没有虚函数,但是又能实现接口的方式,是实实在在的接口,里面有纯虚函数,也即是非侵入式的实现接口,上面宇宙最强悍的fmt就是用到这里的技术

vistor模式和抽象工厂的解耦合,或者又叫,multi dispatch

类型安全的消息,一条消息就代表了一种函数调用,不是win32的那种一点也不安全的类型系统,然后可以向任何类发送消息,动态添加消息的反应,消息队列,消息和消息参数的保存,actor,command模式,redo或undo的轻松实现,消息广播

空基类优化的运用,除了多继承(ATL)或者内嵌类(MFC),还有其他方式,那是以组合方式,通过少量的模板和少量的宏,通过搭配组装(多继承空基类)各种基类,就能完成一个com组件

消息系统的构建,gui框架的编写

........

博大精深的c++!只是想说,上面的一切,在C++下全部都是可行的,当然,宏,template,多继承必须大用特用,只是,奇妙的是,主类的内存布局却很干净,甚至可以没有虚函数
不知道有生之年能否填完坑,以之为励吧!
c++的同学们也充分发挥想象力吧,太多的奇技淫巧了。

posted @ 2016-05-09 20:36 华夏之火 阅读(1324) | 评论 (8)编辑 收藏

仅列出标题
共5页: 1 2 3 4 5 

导航

统计

常用链接

留言簿(6)

随笔分类

随笔档案

搜索

积分与排名

最新评论

阅读排行榜

评论排行榜