• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            huaxiazhihuo

             

            stl的缺陷抽象不足

            總的來說,stl整個的設(shè)計還是很有水準(zhǔn)的,抽象度非常高,采用泛型template手法,回避面向?qū)ο罄锩娴奶摵瘮?shù),回避了繼承,做到零懲罰,達到了非侵入式的要求(非侵入式遠比侵入式要好,當(dāng)然設(shè)計難度也更高出許多)。高性能、可擴展,容器提供迭代器,而算法則作用在迭代器上,容器與算法之間通過迭代器完全解耦,同一種算法可用于多種容器,只要該容器的迭代器滿足其算法的要求;而同一個容器,又可被多種算法操作。更重要的是,容器與算法之間完全是開放的,或者說整個stl的體系是開放式,任何新的算法可用于已有的容器,任何新的容器又可被已有的算法所運算。然后,考慮到某些場合下容器的內(nèi)存分配的高性能要求,分配器allocator也是可以替換,雖然逼格不高。此外,容器算法體系外的邊角料,比如智能指針、anyiostream、復(fù)數(shù)、functio等,也都高性能、類型安全、可擴展,基本上都是c++風(fēng)格量身定制,這些都無可厚非。真的,stl作為c++的基礎(chǔ)庫,已經(jīng)很不錯了。只是,依個人觀點,當(dāng)然,以下純屬一家之言,某些情況下,stl可以做得更好,或者說api的使用上,可以更加清爽。以下對stl的非議,似有雞蛋里挑骨頭之嫌,吹毛求疵,強挑毛病。

             

            軟件框架或者說庫的設(shè)計,是由需求決定的。脫離需求的設(shè)計,不管多精致,代碼多漂亮,一切都是廢物,都是空談。程序庫所能提供的功能,當(dāng)然是越強大越好,可擴展,高性能,零懲罰,這些明面上的概念自然重要。殊不知,程序庫不能做的事情,或者說,其限制條件,或者說程序庫對外界的依賴條件,也很重要。一個基礎(chǔ)庫,如果它敢什么都做,那就意味著它什么都做不好。事情是這樣子的,如果只專注于某件目的,某些條件下的運用,往往可以獲得更大的靈活性或者獨立性。首先,代碼必須很好地完成基本需求,在此基礎(chǔ)上,才有資格談?wù)擖c什么別的更高層次的概念。

             

            上文提到stl由于對動態(tài)類型的排斥,所導(dǎo)致的功能殘缺以及二進制復(fù)用上的尷尬,如果stl愿意正面在動態(tài)類型做完善的考慮,相信stl的格局將會大很多,動態(tài)類型的話題,本文就不再過多重復(fù)了。當(dāng)然,動態(tài)類型的引入必須滿足零懲罰的必要條件,c++的程序庫,所有不符合零懲罰的概念,最后都將被拋棄。所謂的零懲罰,就是不為用不到的任何特性付出一點點代價。請注意,這里的零懲罰必須是一步都不能妥協(xié)。

             

            比如說,字符串實現(xiàn)的引用計數(shù)、短字符串優(yōu)化這些奇技淫巧,就不是零懲罰,短字符串優(yōu)化的懲罰度雖然很輕微,每次訪問字符串內(nèi)容時,都要通過字符串長度來確定其數(shù)據(jù)的地址,長度小,字符串就放在對象本身上,從而避免動態(tài)內(nèi)存分配。長度大,字符串就放在對象外的堆內(nèi)存上。短字符串優(yōu)化空間上的懲罰,字符串對象的占用內(nèi)存變大了,到了長字符串的時候,字符串對象里因為短字符串的內(nèi)存空間就沒有任何價值。在32位機上,字符串對象可以只包含內(nèi)存分配器地址,字符緩沖起始地址,字符長度,緩沖的大小,滿打滿算,也就16個字節(jié)。而短字符串優(yōu)化,就可能要用到32個字節(jié)。其實,如果有一個高性能的內(nèi)存分配器,短字符串優(yōu)化完全就可以沒有任何必要。算了,扯遠了,我們還是回到stl的設(shè)計思路上吧。

             

            大家都知道,stl的字符串其實頂多就是一個字符緩沖管理對象。都98年的標(biāo)準(zhǔn)庫了,完全就沒有考慮字符編碼的需求,真是奇怪之極,令人發(fā)指的完全偷工減料。是啊,字符編碼不好搞,但是既然有這個需求,就必須支持啊,鴕鳥政策是行不通的。雖然說框架上設(shè)計可以既然做不好,那就完全都不做。但是,作為字符串組件,不鳥編碼,你真的好意思以字符串自居。撇開編碼,string居然有一百多個函數(shù),更讓人驚喜的是,這一百多個函數(shù)用于日常的開發(fā),還遠遠不能滿足需求。仔細看,這一坨函數(shù)大多數(shù)僅僅是為了性能需要上的重載,為了避開臨時string對象搞出來的累贅。所以,stl里面必須要有一個只讀的字符串,不涉及任何動態(tài)內(nèi)存分配,也即是c++17string_viewstring_view里面有一個很良好的性質(zhì),string_view的任何一部分還是string_view(不同于c語言字符串的以零結(jié)束,必須帶零的右部分才是字符串),然后string_view就可以做只讀字符串上的運算,比如比較,查找,替換,截取等,分?jǐn)?/span>string里面大部分的函數(shù)。很奇怪的是,string_view這么有用的概念,居然要到c++17里面才有,都不知道stl委員會的人才在想什么。由此可見,如果class里面的成員函數(shù)如果過多,好比一百多個,那么其設(shè)計思路就一定大有問題,甭管它的出處來自何方。

             

            同理可得,只讀數(shù)組array_view也是很有用的概念,它是內(nèi)存塊的抽象。array_view的任何一部分都是array_view,不同于string_viewarray_view僅僅是長度不能變,但是其元素可修改,可以把array_view看成其他語言的數(shù)組,但是array_view不能創(chuàng)建,只能從vector或者c++數(shù)組獲得,或者又可以看成是切片,array_view本身可以有排序和二分查找的成員函數(shù)。Array_view可以取代大多數(shù)vector下的使用場合。很奇怪的是,這么強有力地概念,c++17上居然就可以沒有。差評!另外,我想說的是,對于排序二分查找,就僅僅用于連續(xù)內(nèi)存塊上就好了,其他地方則可免就免,搞那么多飛機干什么,stl在排序二分查找的處理上顯然就是過度抽象。

             

            或者有人反對,array_viewstring_view不就是兩個新的容器,把它們加進stl里,不就行了,stl體系設(shè)計完備,絕對對外開放。不是這樣的,array_viewstring_view的出現(xiàn),嚴(yán)重影響現(xiàn)有的stringvector的設(shè)計,這兩者必須基于array_viewstring_view的存在整個重新設(shè)計。

             

            Stl就是對良好性質(zhì)的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu)缺乏抽象,容器的設(shè)計只到迭代器為止,提供迭代器之后,就高高興興對外宣稱完成任務(wù),不再深入地挖掘,可謂固步自封,淺嘗輒止。在stl的世界觀里面,就只有迭代器,什么都搞成迭代器,什么都只做到迭代器就止步不前,可悲可恨可嘆!在其他基礎(chǔ)容器的設(shè)計上,缺乏深入的考慮,罔顧需求,罔顧用戶體驗。對于鏈表的定位,就足以體現(xiàn)其眼光的狹隘。

             

            眾所周知,單向鏈表的尾部也是單向鏈表,可類比haskell或者lisp的列表,這么強有力的好概念,stl里居然完全沒有單向鏈表,更別說凸顯此概念。當(dāng)然,單向鏈表里面只有一個節(jié)點指針,不能包含內(nèi)存分配器的,也不能有元素計數(shù)器,而且生命周期也要由用戶控制,但是,用戶控制就用戶控制,這一點都不要緊,特別是存在arena allocator的情況下,拼命的new單向鏈表,最后由arena allocator來統(tǒng)一釋放內(nèi)存好了。總之,stl太中規(guī)中矩,對于離經(jīng)叛道的idea,完全就是逃避就是無視,對于動態(tài)類型的處理,也是這種態(tài)度。Stlallocator的處置,太過簡單粗暴,一步錯,步步錯。

             

            而雙向鏈表,在付出O(n)的訪問代價后,在為每個元素都要付出前后節(jié)點的內(nèi)存占用后,應(yīng)該得到咋樣的回報呢?顯然,stllistO(1)插入,O(n)通過迭代器刪除元素,無論如何,完全不能接受,回報太少。首先,O(1)刪除元素,不能妥協(xié)。為達此目的,我們先開發(fā)一個隱式侵入式要求的循環(huán)鏈表,它不關(guān)心元素的生命周期,任何插入此鏈表的元素,其首地址之前必須存在前后節(jié)點的指針。然后,鏈表本身的首兩個字段是頭節(jié)點和尾節(jié)點,內(nèi)存布局上看,循環(huán)鏈表自身就是一個鏈表節(jié)點,一開始,鏈表為空,其首尾字段都指向自身。這種內(nèi)存布局下的循環(huán)鏈表和其節(jié)點的關(guān)系非常松散,節(jié)點的插入和刪除,只需要修改其前后節(jié)點的前后指針的值,完全不需要經(jīng)過鏈表本身來完成操作。要刪除元素時,只要往前爬兩個指針的位置,就得到包含此元素的節(jié)點,進而時間O(1)上刪除節(jié)點,何其方便。顯然,循環(huán)鏈表不能包含節(jié)點數(shù)量,否則每次刪除插入節(jié)點,都要加11鏈表的計數(shù)器,節(jié)點和鏈表就不能徹底的解耦。這種內(nèi)存布局上的循環(huán)鏈表,就可支持多態(tài)了,比如,比如,xlist<Base> objects,可把Derived1類型的對象d1Derived2類型的對象d2,插入到循環(huán)鏈表xlist里,只要d1d2的前面保留前后節(jié)點指針的內(nèi)存空間。

             

            然后,封裝這個裸循環(huán)鏈表,用allocator管理其節(jié)點元素的生命周期,好像stllist那樣,創(chuàng)建刪除節(jié)點元素。封裝過得鏈表,節(jié)點和鏈表的關(guān)系就不再松散,因為節(jié)點必須通過鏈表的allocator來分配內(nèi)存回收內(nèi)存。但是,O(1)時間的刪除節(jié)點,完全是沒有任何問題。并且也能支持多態(tài),可插入子類對象。相比之下,可見stllist有多弱雞,簡直不知所謂。

             

            不管怎么說都好,stl里面對字符串的支持很薄弱,要有多不方便就有多不方便,雖然比C要好多了,這個必須的。當(dāng)然,有人會辯解,很多操作很容易就可以添加進去的,但是,如果標(biāo)準(zhǔn)庫支持的話,畢竟情況會好很多,不一定要做成成員函數(shù),之所以遲遲未添加這些常用的字符串函數(shù),懷疑是因為找不到合適的方式添加這些操作。字符串的操作可分為兩大類,1、生成字符串;2、解析字符串。這兩大類,都是格式化大顯身手的地方,也即是sprintfscanfc++下的格式化函數(shù),可以是類型安全,緩沖不會溢出,支持容器,支持成員變量名字。這樣子,通過格式化,可以吸收大部分的字符串操作。可惜,stl對于反射的排斥,功能強大的格式化處理是不會出現(xiàn)的,而字符串操作,也將永遠是stl的永遠之痛。堂堂c++唯一的官方標(biāo)準(zhǔn)庫,居然對最常用(可以說沒有之一)的編程任務(wù)字符串操作支持如此灰頭土臉,真是要笑死人了。為什么這么說,因為本座的庫早就實現(xiàn)了這些想法(包括string_viewarray_view,不帶allocator類型參數(shù)的各種容器),字符串的處理,簡直不要太方便,比之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要實現(xiàn)這樣的效果,不知道要寫多少惡心的代碼,反正很多人都已經(jīng)想吐了。有了格式化之后,日子美好了好多。對了,上面的vector<byte>可換成list<byte>,代碼完全都可以成立。Fmt的第一個參數(shù)表示格式化結(jié)果的目標(biāo)輸出對象,可以是文件,標(biāo)準(zhǔn)輸出stdoutgui的文本框等。同時,Scanf的第一個參數(shù)表示格式化的輸入源,可以是stdin,文件等。總之,FmtScanf這兩個函數(shù)就可以概括所有的格式化操作,這兩個函數(shù),基本上可以解決滿足日常上大多數(shù)關(guān)于字符串的操作。其實格式化的實現(xiàn),離不開運行時類型信息的支持,本座要有多大的怨念,才會一而再地抱怨stl在反射上的無所作為。

            至于iostreamlocale本座就不想批評太多,免得傷心難過,因為iostream竟然出自c++老父bs之手,必須是精品,某種意義上,確實是!


            啰里啰嗦一大堆不滿,還沒有寫完。后一篇的文章,主角是迭代器,整個stl的大亮點,同時也是大敗筆。既造就了stl框架的靈活性,同時也導(dǎo)致stl函數(shù)使用上的不方便,特別是stl算法函數(shù)的冗余,非正交,不可組合。你看看,findfind_ifremove, remove_copy, remove_copy_if, remove_if,……,難道就不覺得面目可憎,低逼格,心里難受,堂堂大c++標(biāo)準(zhǔn)庫算法的正兒八經(jīng)的函數(shù),標(biāo)準(zhǔn)會要有多扭曲的審美觀,才會這樣設(shè)計如此丑陋的接口,難道就沒有一點點的羞恥心理!這種接口怎么可以拿出來見人見證,丟人現(xiàn)眼。

            posted @ 2017-07-09 11:35 華夏之火 閱讀(1193) | 評論 (5)編輯 收藏

            非完美的stl

                   C++類庫開發(fā)之難,舉世公認,最好的證據(jù)就是,1983年到現(xiàn)在,面世幾十年,就沒有一個正兒八經(jīng)的基礎(chǔ)類庫。是啊,零懲罰,要高性能,要跨平臺,要可擴展,要人性化,又沒有垃圾回收的支持,又沒有運行時類型信息可用,……,這些方方面面的因素糾結(jié)在一起,就好像一個巨大的意大利面線團,真的是眾口難調(diào)至極。相比C#,java,php等,python等雜碎,它們面世不多久,馬上就有官方的標(biāo)準(zhǔn)庫,你要說這些雜碎的標(biāo)準(zhǔn)庫有多好,那也未必,問題是就有大量人馬心悅誠服高高興興地用之于開發(fā),沒有什么所謂的破心智包袱影響開發(fā)效率,甚至有人堅持認為直接用c開發(fā),開發(fā)速度都可以快過c++。哪像c++的破事一大坨,總之就是沒有一個好的基礎(chǔ)庫,能夠讓所有的c++開發(fā)者大爺滿意。你要說這些c++大爺難侍候,也未必,因為的確就是,不管怎么嘔心瀝血搗鼓出來的庫,確實就是是存在這樣那樣的問題,以至于后面的大量使用中,缺陷擴大越來越明顯,難以忍受。

            c++之父一直在重復(fù)強調(diào),c++本身美過西施,美得像楊玉環(huán),c++本身沒有問題,只是欠缺好用的基礎(chǔ)庫。問題是好用的基礎(chǔ)庫千喊萬喊,遲遲就是不肯露面。這種情況下,就很讓人懷疑c++的存在意義了。因為很明顯的事實,其他的后生語言早就有龐大嚴(yán)謹(jǐn)?shù)臉?biāo)準(zhǔn)庫,就你c++諸多借口,搞不出來合格的基礎(chǔ)庫,難道不是c++語言本身就存在重大缺陷,所以才有此困境。很多c++的老殘黨(包括本座),都很贊同c++之父的觀點,c++本身很好,就是欠缺好用的基礎(chǔ)庫。因此大力出奇跡,集整個c++界的精英,花多年的研發(fā),終于奮斗出來stl這個“精品”,另外,還準(zhǔn)備了一個候補的boost,以滿足非正常性的需求。

            平心而論,stl還是相當(dāng)不錯的,高性能,可擴展,零懲罰,跨平臺等,基本上都滿足要求了。除了二進制不能共用,除了編譯速度慢,除了代碼膨脹,除了出錯的時候,可能鋪天蓋地的錯誤,這也是沒有辦法的事情,世上哪有十全十美之事。總之,在基礎(chǔ)設(shè)施嚴(yán)重施缺乏的c++上面,能夠做出來這么一個玩意,已經(jīng)很不容易了。最顯然的事實,面對著stl,除了一小撮亂黨,廣大勞動群眾普遍都認可stl。只是,既然stl是c++里面如此官方的基礎(chǔ)庫,就有必要接受更高標(biāo)準(zhǔn)的考驗。而最終,stl整個的設(shè)計,也不可避免地,也絕非完美。這由此可見,c++基礎(chǔ)庫開發(fā)的難度。

            stl里面的字符串,編碼,iostream,locale,allocator,algorithm里面算法函數(shù)的重復(fù)(非正交)等的問題,都只是表象。根子上考察,stl的設(shè)計思路上犯了左傾和右傾的問題。具體表現(xiàn)如下:
            1、對動態(tài)類型的畏懼,對靜態(tài)類型的過度擁抱。這個問題在c++11之后,有一定程度的改善(出現(xiàn)了shared_ptr, any, variant,內(nèi)里用到動態(tài)類型,起碼有virtual的關(guān)鍵字使用)。最明顯的表現(xiàn)就是,把內(nèi)存分配器allocator做成靜態(tài)類型信息,由此造成的麻煩,真是罄竹難書。同一個整型的vector,因為使用不同類型的allocator,也即是,vector<int, xalloc>和vector<int, yalloc>居然分屬不同的類型,然后有一個函數(shù)要處理整型數(shù)組,要么只能做成模板函數(shù),放在頭文件上,c++原本就編譯速度龜慢,再這樣玩,簡直雪上加霜;如果函數(shù)堅持放在cpp文件里面,就只能處理專門的allocator的整型vector。基本上,用stl打造的公共代碼,都要做成頭文件的共享方式,然后每次小小修改,都要引起連鎖的編譯雪崩,大型的c++項目,對于頭文件的修改,考慮到感人的編譯速度,從來都是非到不得已的時候,能不動就不動。豈有此理,天理何在。c++17,標(biāo)準(zhǔn)庫終于接受多態(tài)的allocator,這算是對過去左傾激進的糾正。某種程度可以上改善這個問題,因為到時候就可以只專門接受多態(tài)的allocator,只可惜,還不完備。

            考慮批量分配arena類型的allocator,理想情況下,對于在此arena allocator上分配的對象,假如僅僅涉及到內(nèi)存問題,其實大多數(shù)情況下,析構(gòu)函數(shù)做的就只是釋放內(nèi)存。那么完全就可以不必苦逼的一個一個調(diào)用對象的析構(gòu)函數(shù),僅僅把arena allocator的內(nèi)存歸還給系統(tǒng)就好了,這對于運行性能的改善,意義重大,本座測過,真是快了很多。問題是,現(xiàn)有stl的體系下,不能保證容器的元素也使用和容器一樣的allocator,或者說,容器的allocator對象無法傳遞給它的元素,讓容器元素也使用同一個allocator對象來分配內(nèi)存。比如說,vector<string>,vector和string的allocator都用polymorphic_allocator,但是,vector的allocator對象和string的allocator可能不是同一個。這樣子,我們就不能僅僅簡單的歸還allocator對象內(nèi)存,而必須像過去那樣子,對vector<string>里面的每一個string都調(diào)用析構(gòu)函數(shù)來歸還內(nèi)存了。差評!所以,一開始,allocator就不應(yīng)該成為模板參數(shù)。另外,stl對allocator的粒度也考慮不周。allocator的迥異應(yīng)用場合起碼有幾種:1、靜態(tài)allocator,專門在main函數(shù)運行前的使用,用于生成元數(shù)據(jù),這些元數(shù)據(jù)不必一一析構(gòu),主函數(shù)結(jié)束后,統(tǒng)一一次性釋放;2、全局的allocator,考慮多線程考慮并發(fā);3、scope,可以在一個任務(wù)下使用,任務(wù)完畢,統(tǒng)一釋放,這里包括函數(shù)或者協(xié)程;4、gui下的allocator等;只可惜,stl的allocator就只關(guān)注全局的allocator。

            既然stl對allocator都可以搞成靜態(tài)類型的鬼樣子,那么整個stl對運行時類型信息的忽視,逃避,就可想而知了。typeid得到的type_info,除了起到類型的唯一標(biāo)識符的作用(動態(tài)庫下,同一種類型的type_info可能還不一樣),并得到類型的名字之外,就不知道這個type_info還有什么鬼用。即便是這么一點小功能,還是能用于很多地方的,比如,any,variant,雙分派(double dispatch),由此可見運行時類型信息的重要性。

            動態(tài)類型信息,也即是反射的重要性,一點都不亞于靜態(tài)類型信息。意義重大,有了反射,我們就可以將類型當(dāng)成變量,到處傳來傳去,也可以保存起來,供后面使用,這里可做的文章,可挖掘的潛力太多了。假如c++的反射信息完善的話,很多頭文件上的模板代碼實現(xiàn)就可以放到源文件里面,模板函數(shù)僅僅是提取一下靜態(tài)類型的運行時對象,類型擦除,具體實現(xiàn)代碼就可以放到cpp代碼里面去。然后,虛模板函數(shù)也可以成為可能了。可以用來創(chuàng)建對象,析構(gòu)對象,消息發(fā)送,非侵入式的接口,序列化……,甚至,連多繼承也都是多余(當(dāng)然,多繼承還是很有用,只是這貨不應(yīng)該出現(xiàn)在正式的場合下)。最典型的例子,格式化printf,通過c++11的variadic template,提取類型的運行時類型對象再連同入?yún)⒌牡刂罚涂梢詫崿F(xiàn)現(xiàn)在c庫里面的那個弱雞sprintf,類型安全,緩沖安全,高性能的效果,不但類型可擴展,連同格式化的控制字符都可擴展,甚至還能支持變量名字。stl里面的iostream、locale的設(shè)計成這個鬼樣子,也是因為運行時的缺失導(dǎo)致。c++里面要妥當(dāng)?shù)靥幚砗米址幋a、字符串、文件流、locale這幾者的關(guān)系,絕對不是一件容易的事情,所以也難怪stl在這里的一塌糊涂。看過iostream,locale的實現(xiàn)源碼,大家都說不好,大家都很難受,簡直可以和mfc媲美,這是真的。

            c++的反射可以做到零抽象,也即是,只對必要的類型必要的信息做反射,不像java或者C#,不管是什么類型,不管是信息,一些很明顯就是無關(guān)緊要的臨時的東西,,不管三七二十一,全部一股腦兒都反射起來。甚至,c++的反射,還能添加用戶自定義的反射信息,甚至,還能運行時修改反射數(shù)據(jù)。這里,C#、java等,除了attribute或者注解,就別無他法了。反射的意義就在于,它提供了統(tǒng)一的接口,將類型信息全部集中存放在同一個地方,任何關(guān)于類型的運行時信息,全部被標(biāo)準(zhǔn)化公理化。有了完善的反射信息,c++里面做一個eval都手到擒來。說白了,反射就是靜態(tài)類型語言里把“代碼做成數(shù)據(jù)”的最重要機制(沒有之一),雖然比之于lisp的“代碼即數(shù)據(jù)”弱一些,但是已經(jīng)可以應(yīng)付99%以上的需求了。甚至可以說,c++的基礎(chǔ)庫遲遲未出現(xiàn)的原因就是因為反射的缺席而導(dǎo)致的(當(dāng)然,沒有合適的內(nèi)存管理機制也是重要原因)。而可惜,stl對運行時這一塊的關(guān)注,不到%1,這真是令人扼腕嘆息至極。

            2,stl的抽象缺陷:臆造抽象,過度抽象,抽象不足,想當(dāng)然的抽象,大部分的精力都花在刀背上,或者說是很形式化的學(xué)術(shù)研究。
            突然發(fā)現(xiàn)文章已經(jīng)很長了,就先打住,以后有空再好好發(fā)揮。對了,cppblog人氣太冷清,門可羅雀。再這樣下去,本座只好轉(zhuǎn)戰(zhàn)知乎了。

            posted @ 2017-07-07 16:52 華夏之火 閱讀(1243) | 評論 (6)編輯 收藏

            完備的運行時類型信息

            眾所周知,碼猿寫代碼,自然要求嚴(yán)謹(jǐn)周密,殊不知想象力也很重要。本座閱碼幾十年,很是感概很多碼猿的腦洞被大大禁錮,鮮有人能越雷池一步,特別是c++的同學(xué),連同委員會的那一坨老頭子,都很讓人無語至極,出自這些人的作品,都是一個死魚眼睛樣子,千人一面,毫無靈動之生趣可言。stl,boost這些庫都是這樣子(雖然它們確實可以完成大多數(shù)日常任務(wù)),更別說其他的庫,沒有什么讓人耳目一新之處。

            就說說動態(tài)類型信息這塊,又或者說是反射。自然,語言本身提供的廢物type_info就懶得說了,除了證明c++也東施效顰,也能支持動態(tài)信息之外,就別無用處了,有誰會正兒八經(jīng)的用type_info做點正兒八經(jīng)的事情呢。因此,各路人馬紛紛上陣,都要彌補c++在運行時類型信息上的缺失。因為類型的反射信息實在太重要,或者說,反射的用武之地太多太多,表面上很多事情不需要反射,或者字面代碼上就看不到反射的痕跡,但是內(nèi)里的實現(xiàn),大把大把的反射在發(fā)光發(fā)熱。c++堅持不在動態(tài)信息上給予一點點多余的支持,并不表示c++就不需要反射了,看看標(biāo)準(zhǔn)庫這個極力回避動多態(tài)的典范,是一個怎樣的失敗作品,嗯,這個以后再談吧。假如stl一開始就沒有如此大力排斥動多態(tài),你看看就連內(nèi)存分配的allocator都可以做到靜態(tài)類型信息里面(最新版的c++終于也要接受多態(tài)的allocator,c++界居然一片歡呼鼓舞,真是悲哀),今時今日的c++就不會在很多領(lǐng)域上到處割地求和。

            總的來說,現(xiàn)在市面上的c++反射庫,都是侵入式,都學(xué)著mfc那一套,都是要求繼承自一個基類Object,然后才能對外提供反射信息的功能,先不說它們提供的類型信息是否完備,這樣子就把用途廣泛限制死在一個很窄很窄的小圈子里面了。這些反射庫,1、不能反射基本類型,int、char、double、const char*、……等;2、不能反射非繼承自O(shè)bject的class或者struct,3、也不能反射模板類,比如vector<int>、list<vector<vector<int>>>。雖然typeid千般弱雞,但也非一無是處,起碼非侵入、平等、多態(tài)。所以,理想的反射,應(yīng)該像c++原生的typeid那樣無色無味:1、非侵入式的;2、可以對所有的類型都提供反射,基本類型、非Object系的struct或者class、template類型的;3、多態(tài)的,只要改類型需要運行時的類型識別,那么就返回其本身的類型(子類),而非字面上的聲明類型;4、支持類型參數(shù),也即是說,以類型傳遞給該函數(shù)時,就返回相應(yīng)的類型信息對象。

            說得具體一點,我們要求的反射庫是這樣子的。當(dāng)然,首先要有一個類型信息對象TypeInfo,里面裝滿了關(guān)于對于類型的所有詳細信息。如下所示:可以猜到這種反射下框架,只支持單繼承,這是故意的。
                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;
                    
                };
            然后,就要有一個函數(shù)TypeOf,應(yīng)該是兩個,一個是無參數(shù)的類型模板函數(shù),可以這樣調(diào)用,TypeOf<type>();一個是有一個參數(shù)的類型模板函數(shù),可以這樣調(diào)用,TypeOf(obj)。不管是那一個,其返回結(jié)果都是const TypeInfo*。TypeOf的要做到的事情是,對于每一種類型,有且只有一個唯一的TypeInfo對象與之對應(yīng),不管是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,接下來就全部都是苦力活,就是為每一種類型都專門特化一個詳細描述的類型對象,用宏可以節(jié)省大量的代碼。但是整個反射庫,本座前前后后重構(gòu)了十幾次,現(xiàn)在也還在重構(gòu)之中,終究還是解決了開發(fā)上所遇到的各種事情。比如,序列化(支持指針、支持多態(tài))、對象與xml的互換、對象與json的互換、數(shù)據(jù)庫表讀寫對象、格式化、Any類型、非侵入式接口、消息發(fā)送、字符串生成對象等等。
            其實現(xiàn)方式,概括起來,就是引入間接層元函數(shù)TypeInfoImp專門用于返回一個類型type,type里面有一個GetTypeInfo()的函數(shù)。然后TypeOf調(diào)用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;    //又是間接層,對動態(tài)類型和非動態(tài)類型分別處理
                    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的方式最具靈活性也最能節(jié)省代碼。最起碼,它在自定義的struct或者class就很方便,只要改struct內(nèi)部包含一個GetTypeInfo()的函數(shù),它就可以被納入TypeOf體系中,非常方便。對于模板類型的TypeInfoImp,就要用到哈希表了。比如,對于std::paira的類型信息,如下實現(xiàn),
                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);
                    }
                };
            提取其類型參數(shù)的const TypeInfo*,生成數(shù)組。用此數(shù)組到PodPair的哈希表里面查找,如果哈希表中以有此類型數(shù)組參數(shù)的對象就返回,否則見創(chuàng)建一個添加一條哈希條目,然后返回。每一個泛型類型,比如vector,list,pair都有一個屬于自己的哈希表。
            打完收工。原理很簡單,但是對于工業(yè)級的反射庫,要考慮很多細節(jié),比如,TypeInfo對象的內(nèi)存管理;怎么為enum類型生成一堆字符串,以支持字符串和enume值的互相轉(zhuǎn)換;生成并保存class的構(gòu)造函數(shù)和析構(gòu)函數(shù)指針;命名空間的支持;仿真C#里面的attribute;如何以最方便的方式生成成員字段或者成員函數(shù)信息等等,一句話,就是他媽的體力活。但是,回報是很豐盛的,這里的苦力活做完之后,程序的其他地方上,基本上,就沒有什么重復(fù)相似的代碼,一切的體力工作全部就可以壓在類型信息這里了。

            posted @ 2017-07-05 11:45 華夏之火 閱讀(1240) | 評論 (1)編輯 收藏

            預(yù)處理之正整型

                  雖然通過一系列的奇技淫巧,讓預(yù)處理也圖靈完備一把,但是用預(yù)處理來做計算,真的很吃力不討好。因為預(yù)處理一開始設(shè)計出來的目的,就沒什么野心,原本就僅僅只是為了做簡簡單單的文本替換工作,并沒有想過要成為正兒八經(jīng)的編程語言,即便是最最縮水版腳本語言的功能要求都達不到。只是后來,實在是大量要求要批量自動生成代碼,特別是c++11之前的版本玩什么模板元編程,鋪天蓋地的要有大量相似的代碼。這些代碼用其他工具來生成,當(dāng)然形式會更加漂亮,但是始終還是用原生的預(yù)處理來做這種事情會更加的方便,否則每次修改,都要運行一遍外部工具,都麻煩啊!本人是傾向于用預(yù)處理來生成代碼的。另外,c++11之后,的確原來很多需要宏來生成代碼的場合已經(jīng)不必要了,但是因為c++11的類型推導(dǎo)能力大大加強了之后,發(fā)現(xiàn)又有一大波地方可以用宏來生成代碼了。并不是說C++中的宏是必不可少之物,但是用了宏,真的可以減少很多很多的重復(fù)代碼,起碼紙面上的代碼清爽了很多。    
                      
                  預(yù)處理的原生數(shù)據(jù)類型就只有符號,然后符號只支持##的并接運算,同時,預(yù)處理也能識別并接后的結(jié)果(否則,并接運算就沒意義了),如果是宏函數(shù),就進行調(diào)用操作,如果是宏符號,就替換文本,如果什么都不是,就什么都不做,保留符號。但是這樣的弱雞類型,顯然遠遠不能滿足離經(jīng)叛道的碼猿需要。經(jīng)過大量的宏編程的嘗試之后,可以很肯定一點,預(yù)處理里面只能再模擬出來一種數(shù)據(jù)類型,那就是正整數(shù),雖然通過補碼運算來仿真負數(shù),但是由于預(yù)處理里面的符號不能包含減號(-)字符,當(dāng)然要花大力氣搗鼓負整數(shù)也是可以的,只是使用上也不方便也不直觀,性價比不高,基本上,必須用宏來生成代碼的地方,都可以不需要負整數(shù)的。

                 另外,預(yù)處理也沒有變量類型的概念,不要說強類型,就連弱類型也不是,完全就是無類型。正整數(shù)類型的概念全靠碼猿人肉編譯器來維護,一個循環(huán)的宏代碼生成一般都是來來回回也不知道調(diào)用了多少層宏調(diào)用,任何一個地方出錯,有時候是幾噸密密麻麻的中間失敗代碼(編譯器的預(yù)處理緩沖溢出,棄械投降),有時候就完全沒有輸出,沒有任何一丁點的提示,簡直是大海撈針的找問題。因此,在用宏循環(huán)生成代碼時,必須小心翼翼,步步為營,不得不感慨,正兒八經(jīng)語言里面的類型真是好東西啊。

            其實,數(shù)據(jù)類型并不重要,重要的是數(shù)據(jù)上能夠支持的運算集合以及這些運算能運用的場合。
            好了,回到上文,我們用_ZPP_INC_N搞了10個數(shù),通過復(fù)制粘貼,可以把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的數(shù),就不支持了,那就都是未定義操作。這樣子,通過PP_INC(n),就得到n+1;而PP_DEC(n),則是n-1。比如PP_INC(PP_DEC(9)),其結(jié)果肯定是9了。很好,這樣子,在預(yù)處理中就實現(xiàn)了自然數(shù)自增1和自減1的運算了。另外,對于大于256的數(shù),比如512傳遞給PP_INC,就只得到一個_ZPP_INC_512的符號,完全沒有任何意義。

            然后,兩個自然數(shù)是否相等的判斷,也非常重要,必須支持。但是,在此之前,要實現(xiàn)一個宏函數(shù)PP_NOT,用來判斷入?yún)⑹欠駷?。為0的話,則函數(shù)返回1,否則,就返回0。也即是:
            PP_NOT(0) == 1
            PP_NOT(23) == 0,或者 PP_NOT(var) == 0。
            記住,預(yù)處理提供給我們的原生類型就只有符號和##并接運算,除此之外,別無他物。好像工具太簡陋,能完成目的嗎?不得不佩服有些碼猿的腦洞。以下代碼是這樣運作的,假設(shè)PP_NOT生成以下的調(diào)用形式,先不管PP_ARG1,至于符號~,是這樣子的,可以看成普通的變量名字,它就是占位符,因為預(yù)處理只識別逗號(,),和小括號,至于其他符號,完全無視,那些是C/C++編譯階段才關(guān)心的符號。
            PP_NOT(0) = PP_ARG1(~, 1, 0)
            PP_NOT(n) = PP_ARG1(_ZPP_NOT_n, 0)
            然后,讓PP_ARG1取第二個參數(shù)(碼猿的計數(shù)是從0開始的,也即是,0即是1,1即是2),就完成任務(wù)了。至于_ZPP_NOT_n是什么鬼,那個只是中間生成的臨時符號,可以舍棄。我們只需對_ZPP_NOT_0做特別處理。因此,代碼可以這樣寫了。PP_PROBE()用以生成兩個入?yún)?br />#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不是只需一個入?yún)幔繛楹魏竺孢€要帶省略號,純粹是為了后面各種變態(tài)的運用,取悅編譯器。已經(jīng)用宏來寫代碼了,就不必再遵守什么清規(guī)戒律,只要能完成任務(wù)就行了。

            至于PP_ARG1的實現(xiàn),就很簡單了,如下所示,
            #define PP_ARG0(_0, ...) _0
            #define PP_ARG1(_0, _1, ...) _1
            #define PP_ARG2(_0, _1, _2, ...) _2

            然后通過兩次取反的函數(shù),再補上函數(shù)PP_BOOL,如果入?yún)?gt;0,就返回1,否則返回0,類似于整型到bool的強制類型轉(zhuǎn)換。
            #define PP_BOOL(_X, ...) PP_NOT(PP_NOT(_X))

            有了這些的鋪墊之后,要比較兩個自然數(shù)是否相等,就簡單了。其實沒什么神秘的,就是針對從0到255,重復(fù)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就是將入?yún)⒉⒔映蒧ZPP_x_EQUALS_y的形式,只要x和y相同,也即是說,它們在上面的表格中,那么,道理就如同PP_NOT的實現(xiàn)那樣,最后結(jié)果就是1了。其實,預(yù)處理中沒有判斷這種玩意,只有表格,只有并接,只有查表。所謂的圖靈完備,說白了,沒有玄虛的,就是建表,然后查表。對相等比較取反PP_NOT,自然就得到不相等的判斷函數(shù)。
            #define PP_UN_EQUALS(x, y) PP_NOT(PP_IS(PP_CONCAT4(_ZPP_, x, _EQUALS_, y)))
            再次建表,就可以得到bool運算的函數(shù),或與
            #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

            再準(zhǔn)備一張表格,將字節(jié)映射到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)
            ...
            然后通過模擬計算機組成原理里面的加減乘除的原理,就可以實現(xiàn)四則運算了。對了,整個預(yù)處理庫的代碼都在壓縮包上,功能比boost的預(yù)處理庫強多了,但是代碼卻少了很多,也容易理解多了,所有代碼在vs下面正常運行,其他平臺還沒有測試。代碼包:/Files/huaxiazhihuo/preprocessor.rar

            posted @ 2017-07-04 14:21 華夏之火 閱讀(770) | 評論 (0)編輯 收藏

            預(yù)處理的圖靈完備之引言

            好久沒有光顧cppblog了,現(xiàn)在這里這么冷清了,不免讓人有些傷感,可見c++現(xiàn)在多么的不得人心,也可能是c++的大神去了其他的網(wǎng)絡(luò)平臺,好比知乎。不管怎么樣,始終對c++還是有些感情,也對cppblog有些感情。

            我們還是來討論c++吧,這幾年在c++里面玩代碼自動生成技術(shù),而預(yù)處理是不可避免,也是不可或缺的重要工具。雖然boost pp預(yù)處理庫在宏的運用上很是完善,但是代碼也太多了,而且代碼很不好理解,對此,不免讓人疑惑,有必要搞得那么復(fù)雜,搞那么多代碼嗎?并且,看了boostpp的使用接口后,感覺寫得很不干凈,也不好組合。因此,重新做了一套預(yù)處理的輪子。以下的代碼,假設(shè)在msvc2013以上的版本運行,反正很多人用MSVC的,裝逼的自當(dāng)別論,造出來的輪子,傾向于先支持msvc。

            首先,我們定義一個宏,用來給把入?yún)⒆兂勺址祝@個事情也太easy了,但是,在此,感覺,還是有必要廢話多解釋一下。以下代碼慣例都是,所有可用的宏函數(shù)都是以PP開頭全部大寫,而以_ZPP開頭的全部都是內(nèi)部實現(xiàn),其實還可以做得更難看一點。因為宏函數(shù)是全局的,沒有作用域的概念,并且只是單純的文本替換,死的時候,還不知道怎么死,所以,必須謹(jǐn)慎對待。像是windows.h頭文件那樣,直接用min,max作為宏的名字,雖然用起來很方便,但也不知道制造了多少麻煩,所以,很多時候,包含windows.h時,第一件事情就是undef min和max。

            以下的代碼,可以隨便在某個工程下,隨便建立一個cpp后綴名的源文件,然后按CTRL+F7編譯,不需要F5,就可以看到運行的效果,如果編譯通過,就說明宏基本上正確,測試代碼越多,準(zhǔn)確性就越高。當(dāng)然,你們也可以通過設(shè)置源文件的屬性,讓msvc生成預(yù)處理后的文件,然后用記事本打開那個文件觀看。
            #define PP_TEXT(str) _ZPP_TEXT(str)
            #define _ZPP_TEXT(str) #str
            在c++預(yù)處理宏中,操作符#是將后面跟隨的表達式加上兩個雙引號,也就是字符串。PP_TEXT(str)不是直接定義成#str,而是通過調(diào)用_ZPP_TEXT(str),然后在那里才將入?yún)⒆兂勺址@得有點輾轉(zhuǎn),有點多此一舉,但,其實是為了支持宏的全方位展開,也就是入?yún)tr本身也存在宏調(diào)用的時候,純屬無奈。比如,如果這樣實現(xiàn)
            #define PP_TEXT(str) #str
            那么,對于下面的情況,
            #define AAA aaa
            PP_TEXT(AAA),結(jié)果將是"AAA",而不是"aaa"。因為宏操作符直接是將入?yún)⒆兂勺址瑳]有讓入?yún)⒂幸稽c點回旋的空間,所以只好引入間接層,讓入?yún)⒂袡C會宏展開。后面,很多宏函數(shù)都是這樣實現(xiàn),不得不間接調(diào)用,以便讓宏全面展開。而msvc的宏展開機制更加奇葩,更加不人性化,其間接調(diào)用的形式也更丑陋。這都是沒辦法的事情。
            然后,為了調(diào)試宏,或者測試宏,當(dāng)然,很多時候,調(diào)試宏,還是要打開預(yù)處理的文件來對比分析。我們對 static_assert作一點點包裝,因為static_assert需要兩個參數(shù),c++11后面的c++版本中,static_assert好像只需要一個入?yún)ⅲ菚r就不需要這個包裝了。
            #define PP_ASSERT() static_assert((__VA_ARGS__), PP_TEXT(__VA_ARGS__));
            PP_ASSERT(...)里面的三個點,是不定參數(shù)的宏,而__VA_ARGS__就代表了...所匹配的所有參數(shù),這條語法很重要,要熟練。這里,就不詳細解釋其用法了,后面會有大把大把的宏函數(shù)用到__VA_ARGS__。
            好了,我們可以開始用PP_ASSERT(...)做測試了。
            PP_ASSERT(2+3==5)
            如果,然后編譯這個文件,發(fā)現(xiàn)編譯通過了,比如
            PP_ASSERT(2+3==4)
            編譯的時候,就會報錯誤信息,error C2338: 2+3==4
            好了,測試準(zhǔn)備建立起來,就可以開始肆無忌憚的寫代碼了。一步一步地構(gòu)建c預(yù)處理宏的圖靈完備。
            顯然,當(dāng)務(wù)之急,最根本的宏就是將兩個宏參數(shù)的并接,也即是##運算符,顯然好比#運算那樣子,必須給里面參數(shù)有宏展開的機會,因此要間接調(diào)用,下面是其實現(xiàn)
            #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
            竟然不止一層間接,而是兩層,又多此一舉,是因為發(fā)現(xiàn)在做宏遞歸的時候,一層間接調(diào)用還不能讓宏充分地展開,所以只好又加間接層,也不明白是何原因,也懶得追究了。現(xiàn)在,接下來,當(dāng)然是測試PP_JOIN了。各位同學(xué),可以新建立一個測試文件,那個文件include我們的這個宏函數(shù)。當(dāng)然,也可以在同一個文件里面寫測試代碼,注意分成兩段代碼,上一段寫宏函數(shù),下一段寫測試代碼,目前來看,都可以的,后面再整理。
            PP_ASSERT(PP_JOIN(1+2== 3))
            #define A 20
            #define B 10
            PP_ASSERT(PP_JOIN(A 
            + B, == 30))
            有了PP_JOIN,就可以開始做點其他事情了。比如,計數(shù)器,
            #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
            這里,我們重新又實現(xiàn)了一遍PP_JOIN,這也是沒辦法的事情,后面在重重嵌套的時候,會出現(xiàn)PP_JOIN里面又包含PP_JOIN的情況,這樣會導(dǎo)致宏停止展開了,所以,只好對于每一個要用到JOIN之處,都用自己版本的JOIN。
            這是宏函數(shù)的實現(xiàn)方式,通過并接,文本替換,一一枚舉,才達到這樣的效果,也就是說,我們通過JOIN函數(shù),在宏里面構(gòu)造了一個計數(shù)器的數(shù)據(jù)類型。如果每個宏函數(shù)都這樣寫,豈不是很累。好消息是,只需用這種苦逼方式實現(xiàn)幾個最基本的函數(shù),然后通過宏的遞歸引擎,其他的宏函數(shù)就不需這樣子一個一個苦逼的并接替換了。
            PP_ASSERT(PP_INC(9)==10)
            PP_ASSERT(PP_INC(PP_INC(
            9)) == 11)
            寫測試代碼習(xí)慣了,寫起來就很有意思了,測試通過,也是最激動人心的時刻。
            接下來,要處理msvc里面宏的惡心行為,然后就結(jié)束本引言。
            #define PAIR_SECOND(x, y) y
            PP_ASSERT(PAIR_SECOND(
            1020== 20)
            這樣子,還不錯,下面,再define一個宏函數(shù),讓其返回一個pair,也就是兩個值
            #define MAKE_PAIR(x, y) x, y
            然后,這樣調(diào)用,
            PAIR_SECOND(MAKE_PAIR(1020))
            編譯器馬上就不高興了,warning C4003: “PAIR_SECOND”宏的實參不足
            好像是編譯器沒有先展開MAKE_PAIR(10, 20),然后再調(diào)用PAIR_SECOND,而是直接把MAKE_PAIR(10, 20)整個當(dāng)成一個函數(shù)傳給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函數(shù)實現(xiàn),可以直接當(dāng)它們是JOIN函數(shù),關(guān)鍵是PP_INVOKE(m, args, ...)這里,第一個參數(shù)m是宏函數(shù),第二個是args,是要傳給第一個參數(shù)m的參數(shù)列表,用括號括起來,至于后面的省略號,是有些時候為了取悅編譯器而添加的,也不知道是什么原因,反正這樣子就可以了,懶得追究。垃圾宏,垃圾預(yù)處理,只要能完成功能就行了,c++中,代碼生成代碼,重頭戲在tmp那里,宏只是小小必要的輔助工具而已。然后,這樣調(diào)用,
            PP_ASSERT(PP_INVOKE(PAIR_SECOND, (MAKE_PAIR(10, 20))) == 20)
            編譯通過了,好不容易啊!

            posted @ 2017-01-14 15:01 華夏之火 閱讀(1448) | 評論 (0)編輯 收藏

            lisp的括號

                   lisp(當(dāng)然也包括scheme)的元編程(也即是宏)威力非常強悍,相比之下,c++的元編程(template+預(yù)處理)簡直就是弱爆了,被人家甩幾條街都不止。 當(dāng)然,template的類型推導(dǎo)很厲害,也能生成很多簽名類似的class和function,比其他語言的泛型強多了,但是,template再厲害,也不能生成名字相似的function還有變量。 預(yù)處理可以生成名字相似的變量和函數(shù)。但是,預(yù)處理的圖靈完備是沒有類型這個概念,只有字符串,整數(shù)那個東西還要靠字符串的并接來實現(xiàn)。所以,預(yù)處理沒法得到template里面的類型信息。新版本的c++中有了decltype之后,宏可以通過某種方式以統(tǒng)一的形式來利用類型信息。但是,在代碼生成方面,預(yù)處理還是很弱智,主要的問題在于宏對于自己要生成的代碼結(jié)構(gòu)很難構(gòu)建語法樹,也不能利用編譯階段的功能,比如調(diào)用編譯階段的函數(shù)。 想說的是,很難以在代碼中只用宏來寫一個稍微復(fù)雜的程序,即使做得到,也要吐好幾口老血,還煞難調(diào)試。
                   lisp就不一樣了,宏和語言融為一體,以至于代碼即是數(shù)據(jù)。只要你愿意,完全可以在lisp中只用宏寫代碼,只要愿意,分鐘鐘可以用lisp寫一個dsl,比如loop就是一個專門處理循環(huán)的dsl。甚至,用lisp宏還可以做靜態(tài)類型推導(dǎo)的事情,也非難事。因此,用lisp宏搞基于對象 (adt)也都有可能,從而優(yōu)雅的使用.操作符。比如(+ obj1.item obj2.item) (obj.fun 2 "hello")。你說,lisp宏連.操作符都可以做到,就問你怕不怕。
                   但是,宏再厲害,也不能隨意地搞底層操作內(nèi)存。恰好與c++相反,c++搞底層隨意操作內(nèi)存太容易了,但是元編程的能力就遠遠不如lisp了。
                   emacs是最好玩的ide,注意不是最強大,猿猴隨時可以寫代碼增加改變emacs的功能,馬上見效,不需要任何配置,不需要重啟。因此,elisp也是最好玩的語言了,因為最好玩的ide的腳本語言就是它了,呵呵,主要原因還是elisp是lisp的方言,可以承擔(dān)lisp的很多構(gòu)思,當(dāng)然,完全繼承是不行的,不過,已經(jīng)足夠做很多很多的事情了。
                   不過,本期的話題是lisp的括號,為什么lisp會有那么多的括號,鋪天蓋地,很容易,就一堆一堆的括號擾人耳目,以至于lisp代碼不好手寫,只能忽視括號,依靠縮進。括號表示嵌套,相必之下,c系語言的嵌套就沒那么恐怖了。一個程序,頂破天,最深層都不會超過十層,連同名字空間,類聲明,函數(shù),再到內(nèi)部的for,if,大括號,中括號,小括號。
                   中綴表達式,這個眾所周知了,試比較,1+2-3*4/obj.width,沒有任何括號,依靠運算符優(yōu)先級表示層次關(guān)系。并且,猿猴也習(xí)慣并本能的解析中綴表達式了,因此,代碼看起來一目了然。lisp就很可憐了, (- (+ 1 2) (/ (* 3 4) (obj-width obj))),這里面多了多少括號,在轉(zhuǎn)換成這行簡單算式的時候,還是在emacs下面寫出來的。關(guān)鍵是,雖然前綴表達式?jīng)]有任何運算級別上的歧義,但是,人眼還是比較習(xí)慣中綴表達式了。君不見haskell的括號更少了,其對中綴表達式和符號的運用更深入。關(guān)鍵是,中綴表達式很容易手寫啊。易寫,自然也表示易讀。C#的linq的深受歡迎也因為其好讀,無須在大腦里面建立什么堆棧,linq表達式就是上一個處理的結(jié)果通過.操作符傳遞到下一個運算中,非常順暢,不必返回前面去看看當(dāng)前的操作數(shù)的運算是什么,因為運算符就在眼前了。中綴表達式.操作符,更是滅掉括號的大殺器,比如,obj.child1.child2.value,這里用lisp來搞,4個括號避免不了的。
                   試試將java萬物皆是對象推向極致,然后沒有中綴表達式,1.plus(2).minus(3.mult(4).obj.width)),比lisp要好一些,但也有很多括號了,并且,在minus這里,其括號嵌套也只是減少了一層而已。當(dāng)表達式復(fù)雜起來的時候,這種缺點也要相應(yīng)的放大。
                   變量的就地定義,好像c系的變量要用到的時候才定義這種語法很稀疏平常,沒什么了不起的。但是,到了lisp下面時,就知道這是多么貼近人心的便利啊。每次用到新變量,都要引入let表達式,又或許跑到前面的let語句中寫變量,要么就打斷當(dāng)前的代碼編寫,要么就引入新的一層嵌套關(guān)系。一個狀態(tài)復(fù)雜的函數(shù),很容易就出現(xiàn)好多個let語法塊。而c系的變量就地定義,顯得那么淡定。
                   return,continue,break等語句就可以把后面的語句拉起來一個層次,假設(shè)沒有這些關(guān)鍵字,要用if else語句,那么,這些return,continue,break后面的語句都表示要被包含在一個else的大括號中。
                   lisp里面的特有語句,with-*等宏,都要求嵌套。幾個with-*宏串起來,幾個括號嵌套關(guān)系就跑不了啦。而c++通過析構(gòu)函數(shù)就多么地讓人愛不釋手了,java也可以別扭的用finally來應(yīng)付了。
                   控制結(jié)構(gòu)的并行。像是if,for,while或者是class還有函數(shù)定義等語句,其后面的代碼塊是并列在關(guān)鍵字的后面,這樣就少了一層嵌套。不過這個作用并沒有那么巨大。主要還是前面4點。
                   這樣,就可以模擬其他語言的特性來滅掉lisp的括號。當(dāng)然是要到宏了,loop就是一種嘗試。但是,下面將走得更遠。其實,就是設(shè)計一套新的語法了。
                   假設(shè)這個宏的名字是$block,那么后面的文章就可以這樣做。
                   1 加入一個$操作符。$表示后面的代碼都被收入進去。比如,1+2-3*4/4,就可以寫成($block (-) $ (+ 1 2) (/) $ (* 3 4) 4)。于是,with-*等宏的嵌套就可以用$來代替了。雖然,$的作用好像有些欠缺,功能不完備,但是,只要考慮到括號都是在最外層體現(xiàn)的,那么,$就顯得很有作用了。
                   2 加入let的操作符,表示就地引入變量,其實也即是將變量名字加入上層的(let)的變量列表中,然后在這里插入一條(setq var vlaue)的語句。
                   3 加入if,elif,else,for,switch等語句,于是后面的代碼塊就與之平行了,并且準(zhǔn)備一個(let)的語句,用于給with語句添加變量。可以借鑒loop宏的方式
                   4 return,break,continue等相應(yīng)的實現(xiàn)。
                   5 支持.操作符,所有關(guān)于.的操作,都轉(zhuǎn)換成相應(yīng)的函數(shù)操作,好像以前的cfront在對于成員函數(shù)的支持那樣子。這里就要有靜態(tài)類型推導(dǎo)了,可以通過with語句中加入變量的類型說明,給函數(shù)添加返回類型的標(biāo)簽。有了這些信息,就可以找到obj相應(yīng)的成員函數(shù)的名字,就可以生成對應(yīng)的函數(shù)調(diào)用的form了,這個做起來有點難度。
                   ......
                   以上,除了第5點,其他都可以借鑒loop的代碼來實現(xiàn)。$block里面的代碼,便于手寫,括號也沒有那么面目可憎了。

            posted @ 2016-05-20 11:17 華夏之火 閱讀(2958) | 評論 (0)編輯 收藏

            迭代器的抽象

                  迭代器是好東西,也是猿猴工具箱里面的七種武器之一。代碼中必然要操作一堆數(shù)據(jù),因此就要有容器,有了容器,自然就不可缺少迭代器,沒有迭代器,容器使用上就會非常不方便,并且還必須暴露其內(nèi)部實現(xiàn)方式。比如,在可憐的C語言里面,操作數(shù)組容器就要通過整數(shù)索引來訪問其元素,操作鏈表容器,就要通過while(node->next!=null)這樣的方式來訪問其元素。某種意義上講,整數(shù)索引,node->next這些都是迭代器,只是它們的使用方式?jīng)]有統(tǒng)一起來而已。既然如此,全世界的迭代器的使用方式都統(tǒng)一起來,那么這就是迭代器了。
                  基本上,現(xiàn)代化的語言,都會在語言層面上提供foreach之類的語法糖,其形式不外乎是,foreach(e:c){}。就是這樣,只要提供元素的名字和容器對象。后面跟著循環(huán)體。其思想就是從容器里面取出一個元素,用循環(huán)體對這個元素進行操作。循環(huán)完畢,就完成了對容器里面數(shù)據(jù)的操作。這種語法形式,簡潔得不能再簡潔了。很好很方便,什么額外重復(fù)的代碼都不勞費心了,甚至連類型推導(dǎo)不用做。真的,類型可以推導(dǎo)的時候,就讓編譯器推導(dǎo)好了。代碼里面必須大規(guī)模的使用auto,var這樣的關(guān)鍵字。不要擔(dān)心看不出來變量的類型。變量類型應(yīng)該從變量的名字中體現(xiàn)出來其抽象意義,當(dāng)然,不要搞什么匈牙利命名法,那個太丑陋了。
                  既然語法糖提供了這種對迭代器的支持操作語法,自然而然,只要涉及到一堆數(shù)據(jù)這樣的概念,不必局限于具體的容器(數(shù)組,鏈表,哈希表),文件夾也是一堆數(shù)據(jù),Composition模式也是一堆數(shù)據(jù),數(shù)列,……,等等所有這些,全部都是概念上的一堆數(shù)據(jù),只要提供了迭代器,猿猴就可以很優(yōu)雅的用foreach這樣的語法糖來統(tǒng)一操作數(shù)據(jù),多么方便,多么的多態(tài)。不管這一堆數(shù)據(jù)的內(nèi)部實現(xiàn)方式是什么,后期怎么修改,在foreach這里代碼全部都不會受影響。更何況,對于迭代器,語法上不僅僅提供foreach的便利得無以復(fù)加的甜糖,還有一大堆的標(biāo)準(zhǔn)庫函數(shù)來讓猿猴操作迭代器,什么排序,查找,映射……。更令人發(fā)指的是,C#把迭代器搗鼓得好用得讓人傷心難過悲憤欲絕,而linq語法上還可以把IEnumbale整成monad,可以用來作什么cps的變換。迭代器在手,天下我有。
                  迭代器這個概念的抽象似乎很理所當(dāng)然,但是不然,比如,劉未鵬舉過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將這一切搞復(fù)雜了。當(dāng)然,還有一批C 粉反對,說什么代碼不透明了,隱藏了代碼背后可能的復(fù)雜實現(xiàn)。對于這一簇人的堅持不懈反對抽象的態(tài)度,真不知該說什么好呢?代碼的能力里面,最最重要的事情就是抽 象,通過抽象,猿猴才可以避開細節(jié),將精力集中于更加重要更加復(fù)雜的事情。通過抽象,可以減少重復(fù)的代碼,可以提高類型安全。C++是唯一能在玩抽象概念的同時,又可以兼顧到底層細節(jié)的處理,從而不僅能寫出高效代碼,還能玩出更炫的技巧。很多時候,必須底層玩得越深,抽象的觸角才能伸得越高。
                  其實,迭代器不必依存于容器。而是,先有了迭代器,才會有容器。請謹(jǐn)記,迭代器可以獨立存在。begin和end就代表了一堆數(shù)據(jù)的概念。至于這一堆數(shù)據(jù)是如何存放的,這一切都無關(guān)緊要。基于此,有必要用class來表達一堆數(shù)據(jù)這么一個通用性極高的概念。其實,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概念上就支持一堆數(shù)據(jù)的操作,只要一個類型struct或者class里面有begin()和end()這一對活寶,并且這一對活寶的返回類型是迭代器,那么就可以盡情的享用foreach的甜糖。那么,何謂迭代器。就是支持三種操作的數(shù)據(jù)類型:!=(判斷相等,用來結(jié)束迭代操作),前++(用來到迭代到下一個元素),*(取值)。那么,這就是迭代器了,顯然,指針就是原生的迭代器。雖然,整形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;
            };
            然后,再用一個函數(shù)FromTo(也不知叫什么名字更好),用來生成DataRange。請注意,我們的迭代器怎么實現(xiàn),那都是細節(jié)。最后展示在用戶層代碼都是干干凈凈的function生成的DataRange,甚至連尖括號都不見了。也不用寫具體是什么類型的DataRange,只須用auto讓編譯器自動推導(dǎo)類型就好了。
            // step = 1是偷懶做法,萬一Step的構(gòu)造函數(shù)不能以1為參數(shù)就弱雞了。比如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));
            }
            于是,F(xiàn)romTo(1, 10, 2)就表示10以內(nèi)的所有奇數(shù),可以用for range的語法糖打印出來。
                  這里的FromTo是按照上升狀態(tài)產(chǎn)生一系列數(shù)據(jù),同樣,也可以產(chǎn)生下降的一堆數(shù)據(jù)FromDownTo,如果愿意的話,同學(xué)們也可以用迭代器形式生成斐波那契數(shù)列。不知注意到了,請用抽象的角度理解++和*這兩個操作符。++就是為新的數(shù)據(jù)做準(zhǔn)備進入到下一個狀態(tài),根據(jù)情況,可以有不同方式,進入到下一個狀態(tài),比如上面的ValueIncrementIterator根據(jù)步長遞增到新的數(shù)值,ValueDecrementIterator的++卻是在做減法,甚至還可以做Filter操作;*就是取到數(shù)據(jù),我們可以在*的時候,才生成一個新的數(shù)據(jù),這里從某種意義上來講,其實就是延遲求值;而!=判斷結(jié)束條件的方式又多種多樣。總之,憑著這三個抽象操作,花樣百出,基本上已經(jīng)能夠覆蓋所有的需求了。
                  為了體現(xiàn)這種抽象的威力,讓我們給DataRange增加一個函數(shù)Concate,用于將兩堆數(shù)據(jù)串聯(lián)成一堆數(shù)據(jù)。首先,定義一個游走于兩堆數(shù)據(jù)的迭代器,當(dāng)它走完第一堆數(shù)據(jù),就進入第二堆數(shù)據(jù)。
            //不知道有什么語法能推導(dǎo)迭代器的值類型,所以搞這個輔助函數(shù)。可能寫成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函數(shù)就很好辦了。
                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;
                }
                 這樣,就把兩堆數(shù)據(jù)串聯(lián)在一塊了,是不是很酷呢?用C++11寫代碼,很有行云流水的快感,又有函數(shù)式編程的風(fēng)格。下期節(jié)目繼續(xù)發(fā)揮,給DataRange加入Filter,Map,Replace等操作,都是將一個DataRange變換成另一個DataRange的操作,顯然,這是一種組合子的設(shè)計方式,也是吸收了haskell和linq的設(shè)計思路。某種意義上講,就是給迭代器設(shè)計一套dsl,通過.操作符自由組合其成員函數(shù),達到用起來很爽的效果,目標(biāo)就是僅僅通過幾個正交成員函數(shù)的隨意組合,可以在大多數(shù)情況下代替stl算法的鬼麻煩的寫法。這種dsl的最大好處類似于linq,先處理的步驟寫在最前面,避開了函數(shù)調(diào)用的層次毛病,最外層的函數(shù)反而寫在頂層。其實迭代器這個話題要展開來說的話,很有不少內(nèi)容,比如用stackless協(xié)程來偽裝成迭代器,F(xiàn)oldl,F(xiàn)oldl1,Scan等。當(dāng)然,真要用得爽,還要配合boost中l(wèi)ambda的語法,好比什么_1+30,_1%2,當(dāng)然,那個也可以自己寫,因為C++現(xiàn)在已經(jīng)支持lambda了,所以,自己寫boost lambda的時候,可以剪裁,取其精華,去其糟粕。如果,再弄一個支持arena內(nèi)存批量釋放又或者是Stack風(fēng)格的allocator(線程相關(guān)),那么就更不會有任何心智負擔(dān)了,內(nèi)存的分配和釋放飛快,這樣的動多態(tài)的allocator寫起來也很有意思,它可以根據(jù)不同情況表現(xiàn)不同行為,比如說多線程下,就會用到線程同步,單線程就無須同步,每個線程單獨擁有一個allocator,根據(jù)用戶需要,還能用棧式內(nèi)存分配,也就是分配內(nèi)存時只是修改指針而已,釋放時就什么都不做了,最后通過析構(gòu)函數(shù),將此allocator的內(nèi)存一次性釋放。當(dāng)擁有一個表現(xiàn)如此多樣的allocator,stl用起來真是爽。

            posted @ 2016-05-14 02:10 華夏之火 閱讀(1316) | 評論 (2)編輯 收藏

            c++單元測試框架關(guān)鍵點記錄成員函數(shù)地址

            原則上,C++下最好的單元測試代碼應(yīng)該長成這樣子,用起來才是最方便的
            TEST_CLASS(className)
            {
                
            // 變量
                TEST_METHOD(fn1)
                {
                    
            // 
                }    
                TEST_METHOD(fn1)
                {
                    
            // 
                }
                
            //
            }
            vczh大神的測試代碼是這樣子,這是最方便使用的形式,但因為是以測試方法為粒度,大括號里面就是一個函數(shù)體,所以顯得功能上有些不足。
            TEST_CASE(ThisIsATestCase)
            {
            TEST_ASSERT(1+1==2);
            }
                  當(dāng)然,這里隱藏了很多宏的丑陋實現(xiàn),但是,那又有什么要緊呢。好不好并不是在于用了什么東西,goto,多繼承,宏,隱式類型轉(zhuǎn)換,……,這些,如果能夠顯著地減少重復(fù)性相似性代碼,還能帶來類型安全,然后又其潛在的問題又在可控的范圍之內(nèi),那么,又有什么理由拒絕呢。老朽一向認為,語言提供的語法糖功能要多多益善,越多越好,當(dāng)然,必須像C++那樣,不用它們的時候,就不會帶來任何代價,那怕是一點點,就好像它們不存在,并且它們最好能正交互補。但是,你看看,cppunit,gtest的測試代碼又是什么貨色呢。
                  據(jù)說cppunit里面用了很多模式,其架構(gòu)什么的非常巧妙。反正使用起來這么麻煩,要做的重復(fù)事情太多了,這里寫測試函數(shù),那里注冊測試函數(shù),只能表示,慢走不送。gtest據(jù)說其架構(gòu)也大有講究,值得學(xué)習(xí),用起來,也比cppunit方便,但是,看看TEST_F,什么SetUp,TearDown,各種鬼麻煩,誰用誰知道。一句話,我們其實只需要class粒度的測試代碼,其他的一切問題就都是小case了。
                  當(dāng)然,class粒度的單元測試實現(xiàn)的難點在于收集要測試的成員函數(shù)。這里不能用虛函數(shù)。必須類似于mfc里面的消息映射成員函數(shù)表。也即是當(dāng)寫下TEST_METHOD(fn1),宏TEST_METHOD就要記錄下來fn1的函數(shù)指針。后面跟著的一對大括號體是fn1的函數(shù)體,已經(jīng)越出宏的控制范圍了,所以只能在前面大做文章。下面是解決這個問題的思路。這個問題在C++03之前的版本,比較棘手。但是,所幸,C++11帶來很多逆天的新功能,這個問題做起來就沒那么難了。下面的思路省略其他各種次要的細節(jié)問題。
            首先,我們定義一個空類和要測試的成員函數(shù)的形式。
            struct EmptyClass{};
            typedef void(EmptyClass::*TestMethodPtr)();
            還有存放成員函數(shù)地址的鏈表節(jié)點
            struct MethodNode
            {
                MethodNode(MethodNode
            *& head, TestMethodPtr method)
                {
                    mNext 
            = head;
                    head 
            = this;
                    mMethod 
            = method;
                }
                MethodNode
            * mNext;
                TestMethodPtr mMethod;
            };
            還有提取成員函數(shù)地址的函數(shù)

            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;
            }
            方法是每定義一個測試函數(shù),在其上面就先定義一個鏈表節(jié)點變量,其構(gòu)造函數(shù)記錄測試函數(shù)地址,并把自身加入到鏈表中。但是,在此之前,我們將遭遇到編譯器的抵觸。比如
            struct TestCase
            {
                typedef TestCase ThisType;
                MethodNode
            * mMethods = nullptr;

                TestMethodPtr mTestMethodfn1 
            = GetTestMethod(&fn1);
                void fn1(){}
            };
                  vc下面,編譯器報錯 error C2276: “&”: 綁定成員函數(shù)表達式上的非法操作
                  原來在就地初始化的時候,不能以這種方式獲取到地址。然后,試試在TestCase里面的其他函數(shù)中,包括靜態(tài)函數(shù),就可以將取地址符號用到成員函數(shù)前面。
                  這好像分明是編譯器在故意刁難,不過,任何代碼上的問題都可以通過引入中間層來予以解決。用內(nèi)部類。
            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(){}
            };
                  有多少個測試方法,就動用多少種內(nèi)部類。然后,一旦定義一個測試類的變量,那么這些內(nèi)部類的構(gòu)造函數(shù)就執(zhí)行了,把測試方法串聯(lián)在一塊,逆序,也就是說最后定義測試方法反而跑到前面去了。這樣子就自動記錄下來所有的測試方法的地址。有了這些函數(shù)地址信息,后面怎么玩都可以。包括漂亮的測試結(jié)果顯示,日志記錄,甚至嵌入到vs的單元測試界面中,又或者是生成配置文件,各種花招,怎么方便就怎么玩。這個時候,可以拿來主義,把cppunit,gtest等的優(yōu)點都吸收過來。
                  是否覺得這還不夠,好像有很多事情要做。比如說,測試方法逆序了,在同一個測試類的變量上執(zhí)行這些測試方法,會不會就擾亂類的內(nèi)部信息了,每次new一個測試類,所有的測試方法都要重復(fù)記錄,內(nèi)部類變量要占內(nèi)存……。咳咳,這些都可以一一解決。這里只是用最簡明的方式展示自動記錄測試方法,產(chǎn)品級的寫法肯定大有講究了。
                  可以看到上面的代碼都是有意做成很相似的,這些都是準(zhǔn)備給宏大展身手的。這些低級宏太容易編寫了,任何經(jīng)歷mfc或者boost代碼折磨的猿猴,都完全能夠勝任,這就打住了。對了,這里的自動記錄成員函數(shù)的宏手法,可以大量地使用到其他地方,比如說,自動生成消息映射表,比mfc的那一套要好一百倍,應(yīng)用范圍太廣了。當(dāng)初老朽以為就只能用于單元測試框架的編寫上面,想不到其威力如此巨大,消息系統(tǒng)全靠它了。C++的每一項奇技淫巧和功能被發(fā)現(xiàn)后,其價值都難以估量,好像bs所說的,他老人家不會給c++增添一項特性,其應(yīng)用范圍一早就可以預(yù)料的。對付一個問題,C++有一百種解決方案,當(dāng)然里面只有幾種才最貼切問題領(lǐng)域,但是很多時候,我們往往只選擇或者尋找到另外的那90多種,最后注定要悲劇。

            posted @ 2016-05-11 18:01 華夏之火 閱讀(1543) | 評論 (0)編輯 收藏

            消息發(fā)送雜談

                  最近在看MFC的代碼,雖然這破玩意,老朽已經(jīng)很熟悉了得不能再熟悉了,但是這些破代碼自由其獨有的吸引力,不說別的,單單理解起來就非常容易,比之什么boost代碼容易看多了,單步調(diào)試什么的,都非常方便,堆棧上一查看,層次調(diào)用一目了然。一次又一次地虐這些曾經(jīng)虐過老朽的代碼,也是人生快事一件。平心而論,mfc的代碼還是寫得挺不錯的,中規(guī)中矩,再加上過去九十年代之初,16位的windows系統(tǒng),那個時候的面向?qū)ο蟮腸++的風(fēng)靡一時,完全采用標(biāo)準(zhǔn)c++,能做成這樣,實屬難能可貴,也在情理之內(nèi)。并且,后面壓上com之后,mfc也不崩盤,采用內(nèi)嵌類實現(xiàn)的com的方式,也很有意思。然后,從mfc中也能學(xué)到不少windows gui的使用方式還有各種其他雜七雜八東西,雖然win32已經(jīng)沒落。但是里面的技術(shù)還是挺吸引人,可以消遣也不錯。當(dāng)然,對于新人,mfc不建議再碰了,mfc真是沒飯吃的意思。你想想,一個gui框架,沒有template可用的情況下,而逆天c++11的lambda作為匿名functor,更加不必提了,只有虛函數(shù)和繼承可用,也沒有exception,能搞成mfc這副摸樣,的而且確是精品。其實,后來的巨硬也有救贖,看看人家用template做出來的專為com打造的atl又是什么樣子呢,然后建構(gòu)在atl的windows thunk基礎(chǔ)上開發(fā)的wtl又是怎樣的小巧玲瓏。巨硬在template上的使用還是很厲害的,atl將template和多繼承用的真是漂亮。人家?guī)资昵熬蛯emplate和多繼承用得如此出神入化,反觀國內(nèi),一大批C with class又或者狗粉一再叫囂template滾出c++,多繼承太復(fù)雜了,運算符重載不透明,心智負擔(dān),隱式類型轉(zhuǎn)換問題太多,virtual是罪惡之源萬惡之首,構(gòu)造函數(shù)析構(gòu)函數(shù)背著馬猿做了太多事情,exception對代碼沖擊太大,打斷代碼正常流行,時時刻刻都好像隱藏著不定時炸彈。依本座看來,C++中一大批能夠顯著減少重復(fù)代碼,帶來類型安全的拔高抽象層次的好東西,對于這些C語言衛(wèi)道士而言,都是混亂之物。其實,c語言就是一塊廢柴,抽象層次太低,可以做文章的地方太少了。
                  就以構(gòu)造函數(shù)和類型轉(zhuǎn)換operator為例,來看看怎么用于C的char *itoa(int value,  char *str,  int radix)。
                  itoa的參數(shù)之所以還需要str入?yún)ⅲ鞘且驗镃語言中缺乏返回數(shù)組的語言元素,所以調(diào)用者要提供一個字符數(shù)組作為緩沖用于存放結(jié)果,但是這個多出來str參數(shù)真是沒必要啊,因為語言能力的欠缺,所以只好把這個負擔(dān)壓到猿猴身上。也有些itoa的實現(xiàn)沒有這個累贅str的入?yún)ⅲ莾?nèi)部static的字符數(shù)組,用于存放結(jié)果并返回給上層。這個版本就只有兩個入?yún)⒘耍且蔡话踩耍瑒e提多線程了。假如,有一個函數(shù)fn(char* str1, char* str2),然后,這樣調(diào)用fn(itoa(num1),itoa(num2)),畫面太美了。另外,那個有多余str參數(shù)版本的itoa也好不到哪里去,要勞心費神準(zhǔn)備兩塊字符數(shù)組,然后還要保證參數(shù)傳遞的時候不要一樣。反正C語言的粉絲整天很喜歡寫這些重復(fù)代碼,并且美其名曰掌控一切細節(jié)的快感。
            請看構(gòu)造函數(shù)和類型轉(zhuǎn)換operator怎么解決。太easy了。

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

                ToString(
            int n)
                {
                    
            //轉(zhuǎn)換字符串,結(jié)果存放于text中
                }

                
            operator const char*()
                {
                    
            return text;
                }
            };
                  并且,這里的ToString還可以安全的用之于printf里面呢,因為它本身就是字符串的化身。為什么是ToString,因為它不僅僅要它ToString int,還有double,bool,char,……
                  不好意思,扯遠了,只是想說,框架或者函數(shù)庫的表現(xiàn)能力也要取決于語言本身的表達能力。就好像C#就可以做出linq那樣爽快的框架,java再怎么拼命也搗鼓不出來一個一半好用的linq,C++也不行,但是C++可以搗鼓類似于haskell中的map,filter,fold等,  并結(jié)合linq的后綴表達方式。就好比下面這樣
                  vector<int> nums = {...}
                  Range(nums).Map(_1 * _1).Filter(_1 % 2).CopyTo(dest); // 用了boost中的lambda表達法,因為的確很簡潔,沒法控制。對于復(fù)雜情況,當(dāng)然要用C++11原生的lambda
                  勉勉強強差可滿足吧。如果C++的lambda參數(shù)可以自動推導(dǎo)就好了,不過也沒什么,主要是ide下用得爽。用泛型lambda也能將就。
                  所以,回過頭來,再看看mfc(沒飯吃),就可以了解其各種隱痛了。真的,以90年代的眼光來看,mfc真是做到極致了。mfc不可能走win32下窗口函數(shù)C語言那樣的消息發(fā)送消息反應(yīng)的正路(邪路)吧。窗口函數(shù)這一套,在90年代面向?qū)ο笫⑿械臅r代,絕對不能被忍受,只是到了前幾年,才被發(fā)現(xiàn)其價值原來不菲,這是解耦合砍繼承樹的好手法,老朽在前幾年也跟風(fēng)吹捧窗口函數(shù)的那一套。平心而論,smalltalk的這一套消息發(fā)送的動態(tài)語言,確實是很強有力的抽象手段,我不管目標(biāo)對象能否反應(yīng)該消息,閉著眼睛就可以給你發(fā)送消息,你能反應(yīng)就反應(yīng),不能反應(yīng)就拉倒,或者調(diào)用缺省的反應(yīng)方式,就好像DefWindowProc(職責(zé)鏈模式?),又或者是拋出異常,怎么做都可以。一下子就解開了調(diào)用者和目標(biāo)對象的類型耦合關(guān)系。面向?qū)ο笾校l(fā)送和消息反應(yīng)才是核心,什么封裝繼承多態(tài),那是另一套抽象方式,雖然坊間說這也是面向?qū)ο蟮幕疽兀遣皇牵?dāng)然,這或許也只是個人觀點。
                  或許,從某種意義上講,C++/java/C#一類的成員函數(shù)調(diào)用形式,其實也算消息發(fā)送吧。比如,str.length(),就是給對象str發(fā)送length的消息,然后str收到length消息,作出反應(yīng),執(zhí)行操作,返回里面字符串的長度。靠,這明明就是直接的函數(shù)調(diào)用,搞什么消息發(fā)送的說辭來強辯,顛倒是非黑白,指鹿為馬。可不是嗎?編譯器知道str是字符串類型,知道length成員函數(shù),馬上就生成了高效的函數(shù)調(diào)用方式。在這里,沒有任何動態(tài)多態(tài)的可能,發(fā)生就發(fā)生了,一經(jīng)調(diào)用,動作立馬就行動,沒有任何商量的余地。耦合,這里出現(xiàn)強耦合,調(diào)用者和str綁在一塊了,假如以后出現(xiàn)更高效率更有彈性的string的代替品了,可是沒法用在這里了,因為這里str.length()的綁定很緊很緊。
                  人家消息發(fā)送就不一樣了,動態(tài)的,可以動態(tài)替換對象,替換方法,彈性足足。并且,消息發(fā)送的模式下,對象收到消息,要判斷消息,解析消息,找到消息的執(zhí)行函數(shù),最后才終于執(zhí)行任務(wù)。這么多間接層,每一層都可以做很多很多文章。比如,在消息到達對象之前做文章,就可以搞消息隊列,把消息和參數(shù)暫存起來,這個時候,什么actor模式就大放異彩,至于undo,redo,更加是小菜一碟。然后呢,再給對象安裝消息解析器,把消息和消息參數(shù)轉(zhuǎn)換成其他類型消息。比如原本對象不能反應(yīng)這條消息,但是對消息參數(shù)稍加修飾,然后在發(fā)送給對象,這不就是適配器模式。總之,可操作可挖掘的空間太大了,遠遠不止23條。
                  但是,封裝繼承多態(tài)就一無是處了嗎?不是的,最起碼一點,編譯期間可以報錯。因為的確有很多時候,我們明明就知道對象的類型,明明就知道對象不可能是其他類型,比如字符串,比如復(fù)數(shù),比如數(shù)組這些玩意,無論如何,它們都不需要動態(tài)消息的能力。我們就知道手上的對象就是字符串就是復(fù)數(shù),不可能是別的,并且,我們就是要明確地調(diào)用length函數(shù)。我們就是要編譯器幫忙檢查這里潛在的語法類型錯誤,比如對復(fù)數(shù)對象調(diào)用length函數(shù),編譯器馬上就不高興了。并且,一切都是確定的,所以編譯器也能生成高效的代碼,高效的不能再高效了。對此,消息發(fā)送的面向?qū)ο缶妥霾坏搅耍还苁鞘裁磳ο螅琲nt,string,complex種種,都來個消息發(fā)送。這樣一來,靜態(tài)類型檢查和高效的代碼,就木有了。
            考察一下,面向?qū)ο笥械燃壷郑徊揭徊剑羞M化的階梯。每進化一次,就多了一層間接,類型耦合就降低,就進一步超越編譯器的限制,當(dāng)然,也意味著編譯器幫忙檢查類型錯誤生成高效代碼就弱了一分。事情往往就是,有所得必有所失。少即是多,多即是少。因此,可推得少即是少,多即是多。少始終是少,多始終是多。
                  一切,還是要從C語言說起,C語言中,沒有class,沒有函數(shù)重載。函數(shù)名是什么,最后就是什么。在這種條件下,代碼多了,每個新的函數(shù)名字要考究半天,一不小心,要么函數(shù)名字就會很長,要么函數(shù)名字短了要沖突或者不好理解。但是好處是,最后生成目標(biāo)代碼時,什么函數(shù)名字就是什么名字,所見即所得,沒有異常,不會搗鬼,于是其他各種語言都可以高高興興開開心心調(diào)用。猿猴觀碼,也很清晰。C++也是在這里賺了第一桶金。其實,這么苛刻的條件下,最考究猿猴的代碼架構(gòu)能力,架構(gòu)稍微不好,最后都勢必提早崩掉,前期就可以過濾很多垃圾架構(gòu)。
                  然后就是C with class了,開始在函數(shù)名字上面做文章了。同一個函數(shù)名字依對象類型,開始擁有靜態(tài)多態(tài)能力了。比如,str.length(),這是對字符串求長度。數(shù)組的變量,nums.length(),對數(shù)組求長度。同一個length的名字,用在不同的對象上,就有不同的意義。這如何做到呢,最初,cfront(第一版C++編譯器)的處理方式是,可以說是語法糖,就是在名字和調(diào)用形式上做文章,比如,str.length(),變成,string_length(&str),array_length(&nums)。別小看這點小把戲語法糖,這真是有力的抽象手法。不說別的,就說起名字吧,可以統(tǒng)一length了,無須費思量string_length,list_length了。然后,對象統(tǒng)一調(diào)用方式,str.length(),list.length(),函數(shù)確定這種吃力不討好的事情就交給編譯器去做好啦,解放部分腦細胞。這,的確很好,但是,全局函數(shù)是開放式的,而對象的成員函數(shù)是封閉的,一旦class定義完畢,成員函數(shù)的數(shù)量也就定死了。猿猴最講究東西的可擴展性,不管成員函數(shù)多么方便多么抽象有力,就擴展性而言,就差了一大截,其他優(yōu)勢都彌補不了。語義上看,擴展成員函數(shù)的語法完全與原生的一樣,增加一個簡單的語法形式來擴充,但是多年下來,標(biāo)準(zhǔn)委員會都不務(wù)正業(yè),哎。顯然,編譯器的類型檢查能力和生成的代碼性能,沒有任何減少,但是,猿猴看代碼,不能再所見所得了,必須根據(jù)對象類型,才能確定最終的目標(biāo)函數(shù)。就這么點小改進,當(dāng)時C++馬上就展示其驚人的吸引力。假如,C++只能留在這一層,相信到今天為止,可以吸引到更多的c粉。可是,C++開始叛變。
                  C++的函數(shù)重載,還有操作符重載,外加隱式類型轉(zhuǎn)換和隱式構(gòu)造函數(shù),還有const,volatile修飾,當(dāng)然,代碼可以寫得更加簡潔,編譯器可以做的事情也更多啦,但是函數(shù)的調(diào)用再也不明確了。部分專注于底層的猿猴的弱小的抽象能力把控不住了,不少人在這里玩不動了。此外,命名修飾把最終函數(shù)名字搞得亂七八糟,二進制的通用性也要開始廢了。導(dǎo)致C++的dll不能像C那樣到處通吃。像是狗語言就禁止函數(shù)重載這個功能。大家好像很非難C++的操作符重載,但是haskell還能自定義新的操作符呢。雖然在這里,編譯器還能生成高效代碼,但是,各種奇奇怪怪類型轉(zhuǎn)換的規(guī)則,編譯器也偶爾表現(xiàn)出奇,甚至匪夷所思,雖然一切都在情理之內(nèi)。
                  其實,不考慮什么動態(tài)能力,單單是這里的靜多態(tài),基于對象(俗稱ADT)的抽象模式,就可以應(yīng)付70%以上的代碼了。想想以前沒有靜多態(tài)的C日子是怎么過的。
                  此時,開始兵分兩路,C++一方面是動多態(tài)發(fā)展,表現(xiàn)為繼承,多繼承,虛繼承,虛函數(shù),純虛函數(shù),rtti(廢物,半殘品),到此為止了,止步不前;另一方面是繼續(xù)加強靜多態(tài),王者,template,一直在加強,模板偏特化,template template,varidiac tempalte,consexpr, auto,concept,……,背負著各種指責(zé)在前進,就是在前進。C++企圖以靜態(tài)能力的強悍變態(tài)恐怖,不惜榨干靜態(tài)上的一點點可為空間,累死編譯器,罔顧邊際效應(yīng)的越來越少,企圖彌補其動態(tài)上的種種不足。這也是可行的,畢竟haskell都可以做到。template的話題太龐大了,我們言歸正傳,面向?qū)ο蟆?br />      下面就是被指責(zé)得太多的C++多繼承,虛函數(shù),RTTI,脆弱的虛函數(shù)表,等,這些說法,也都很有道理,確是實情,兼之C++沒有反射,沒有垃圾回收,用上面這些破玩意搗鼓,硬著頭皮做設(shè)計做框架,本來就先天能力嚴(yán)重不足,還要考慮內(nèi)存管理這個大敵(循環(huán)引用可不是吹的),更有exception在旁虎視眈眈,隨時給予致命一擊。更要命的是,多繼承,虛函數(shù),虛繼承,這些本來就殺敵八百自傷一千,嚴(yán)重擾亂class的內(nèi)存布局,你知道vector里面隨隨便便插入元素,對于非pod的元素,不僅僅是移動內(nèi)存,騰出新位置來給新對象安營扎寨,還要一次又一次地對被移動的對象執(zhí)行析構(gòu)拷貝構(gòu)造。沒有這些奇奇怪怪的內(nèi)存布局,vector的實現(xiàn)應(yīng)該會清爽很多。稍微想想,這實在太考究猿猴的設(shè)計能力,其難度不亞于沒有任何多態(tài)特性的C語言了。可以這么說,繼承樹一旦出現(xiàn)虛繼承這個怪胎,整體架構(gòu)就有大問題,毫無疑問,iostream也不例外。不過,如果沒有那么多的動態(tài)要求,好比gui框架的變態(tài)需求,嚴(yán)格以接口作為耦合對象,輔以function,也即是委托,又可以應(yīng)付多起碼15%的局面。其實,必須要用到virtual函數(shù)的時候,將virtual函數(shù)hi起來,那種感覺非常清爽,很多人談virtual色變,大可不必。C#和java還加上垃圾回收和反射,這個比例可以放大很多。在這種層次下,接口最大的問題是,就好像成員函數(shù),是封閉的。一個class定義完畢,其能支持的interface的數(shù)量也就定死了,不能再有任何修改。interface可以說是一個class的對外的開放功能,現(xiàn)實世界中,一種東西的對外功能并不是一開始就定死了的,其功能也在后來慢慢挖掘。但是,C++/java/C#的接口就不是這樣,class定義完畢,就沒有任何潛力可言了。明明看到某些class的能力可以實現(xiàn)某些接口,甚至函數(shù)簽名都一樣,對不起,誰讓你當(dāng)初不實現(xiàn)這個接口。對此,各種動態(tài)語言閃亮登場,或mixing或鴨子類型。接口還有另一尷尬之處,比如,鳥實現(xiàn)了會飛的接口,鴨子企鵝也繼承了鳥,自然也就繼承了會飛的接口,沒辦法不繼承。面對著一個需要IFlyable參數(shù)的函數(shù),我們順利的傳一只企鵝進去,然后企鵝再里面始終飛不起來,就算企鵝在被要求飛的時候,拋出異常,也不過自欺欺人。這種悲劇,就好像有些人很會裝逼,最后一定會壞事。搞出接口這種破事,就是為了讓編譯器做類型檢查的。又有人說,bird應(yīng)當(dāng)分為兩類,會飛的和不會飛的,這的確能解決飛行的尷尬。但是,有很多鳥具備捉蟲蟲的能力,然后又有那么一小撮鳥不會捉蟲只會捉魚,難道又要依據(jù)捉蟲能力再劃分出鳥類。于是鳥類的繼承樹越長越高,畫面越來越美。這分明就是語言能力的不足,把問題交給猿猴了。請謹(jǐn)記,接口就是一個強有力的契約,既然實現(xiàn)了一個接口,就說明有能力做好相關(guān)的事情。再說,既然interface這么重要,于是我們再設(shè)計class的時候,就自然而然把精力放在interface這個對外交流媒介的手段之上了,而忽視了class本身的推敲。class最重要的事情就是全心全意做好獨立完整最小化的事情,其他什么對外交互不要理會。一個class如果能夠完整的封裝一個清晰的概念,后面不管怎么重構(gòu),都可以保留下來。但是,interface會分散這種設(shè)計。接口的悲劇就在于企圖頂多以90分的能力去干一百分的事情,并且還以為自己可以做得好,硬上強干,罔顧自身的極限。往往做了90%的工作量,事情恰恰就壞在剩下來的10%上。
                  于是,狗語言走上另一條邪路,鴨子類型。只要class,不,是struct,這種獨特關(guān)鍵字的品味,只要某個struct能夠完全實現(xiàn)某個interface的所有函數(shù),就默認其實現(xiàn)了這個接口。并且,狗語言還禁止了繼承,代之以“組合”這個高大上的名詞了,但是,細究一下語義和內(nèi)存布局(忽略虛函數(shù)表指針),你媽的,不就是一個沒有virtual繼承的弱多繼承嗎?顯式的繼承消失了,隱式的繼承還存在的,好了,還不讓你畫出繼承樹關(guān)系圖,高高興興對外宣稱沒有繼承了,沒有繼承并不表示繼承的問題木有存在。但是,因為狗語言的成員函數(shù)方法可以定義在class,不,struct外面,其擴展性就非常好了,對于一個interface,有哪些方法,本struct不存在,就地給它定義出來,然后,struct就輕松的實現(xiàn)了該接口,即使原來的struct不支持該接口,以后也有辦法讓它支持,很好很強大。之所以能做到這一點,那是因為狗語言的虛函數(shù)表是動態(tài)生成的。小心的使用接口各種名字,部分人應(yīng)該狗語言用起來會相當(dāng)愉快。可是,你媽,不同接口的函數(shù)名字不能一樣啊,或者說,同一個函數(shù)的名字不能出現(xiàn)在不同接口中。不過,這個問題并不難,不就是不一樣的名字嗎,c語言中此等大風(fēng)大浪猿猴誰沒有見識過。對于狗語言,不想做太多評斷,只是,其擴展性確實不錯,非侵入式的成員函數(shù)和非侵入式的接口,理應(yīng)能更好地應(yīng)付接口實現(xiàn)這種多態(tài)方式,只是,編譯器在上面所做的類型約束想必會不如后者,重構(gòu)什么的,想必不會很方便。自由上去了,約束自然也下來了。聽起來挺美,但是內(nèi)里也有些地方要推敲,反正老朽不喜歡,以后也不大會用上,當(dāng)然,給money自然會用,給money不搞c++都沒問題。老朽還是比較喜歡虛函數(shù)的接口,更何況c++通過奇技淫巧也能非侵入式的給class添加接口。在靜態(tài)語言中搞這種鴨子類型的動態(tài)語言接口,顯得有點不倫不類。
                  然后就是com接口的面向?qū)ο螅耆崛ゾ幾g器對接口類型的約束,自然能換來更大的自由。由于com的語言通用性目標(biāo),所以搞得有點復(fù)雜,但是com背后的理念也挺純潔。老朽猜測com好似是要在靜態(tài)語言上搭建出一個類似于動態(tài)語言的運行平臺,外加語言通用性。其契約很明確,操作對象前時,必須先查詢到對象支持的接口,進而調(diào)用接口的函數(shù)。這里有意思的地方在于面對著一個com對象,你居然沒有辦法知道到它究竟實現(xiàn)了多少接口。
                  最后就是消息發(fā)送了,其能力之強大,誰用誰知道。原則上講,可以看成對象擁有的虛函數(shù)表的方法無窮多,又可以把每一條消息看成一個接口,那么,對象可能就實現(xiàn)了無窮多的接口。你說,面對著這樣對象,還有什么做不出來呢。真用上消息發(fā)送這種隱藏?zé)o數(shù)間接層,就沒有什么軟件問題解決不了的。任何軟件問題不就是通過引入間接層來解決的嘛。現(xiàn)在用上消息發(fā)送這種怪物,就問你怕不怕。沒有免費午餐,自然要付出類型安全的危險和性能上的損失。

            posted @ 2016-05-10 22:40 華夏之火 閱讀(1656) | 評論 (3)編輯 收藏

            挖坑,有空填坑


            先挖坑,計劃寫出一系列文章,探討將c++用成動態(tài)語言,或者函數(shù)式語言,以達到快速開發(fā)的目的,并且在需要優(yōu)化的情況下,又能夠方便快速的優(yōu)化。現(xiàn)在事務(wù)太多,不知道何時能填坑

            宏的圖靈完備,用宏生成代碼,特別是反射,模式匹配,實在必不可少,以至于宏可以與c++的繼承、template、exception等基本組件并列的重要必不可少的補充手段

            最小巧方便使用的單元測試框架,比gtest,cppunit要好用很多

            自定義內(nèi)存管理器,stl中的allocator是作為模板參數(shù)來傳遞,嘗試以tls來傳遞allocator參數(shù),當(dāng)然,必須相應(yīng)的各種容器都要重寫,修改其缺省構(gòu)造函數(shù),拷貝復(fù)制移動拷貝,給元素分配內(nèi)存釋放內(nèi)存等。對了,還有各種容器的反射信息。每種類型的template的容器都有一個typeinfo對象,具體的容器又有自己獨一的typeinfo對象

            完善完備的reflection,也就是,其他language能夠做的反射的事情,這里只要愿意,也可以做到,非侵入式,可以給int,double等基本類型添加反射,給template類型的也添加反射信息,保證每種類型的反射對象是唯一的;

            史上功能最完善的fmt的實現(xiàn),非template,當(dāng)然,外層還需要variadic來包裝,以類型信息。類型安全,緩沖安全,高效,通用。通用的意思是,可以fmt到文件,日志,字符串,文本框控件中;類型安全的意思是,可以是所有的類型都可以fmt,只要該類型實現(xiàn)了相應(yīng)的接口,但是,這種接口是非侵入式的,通過模板特化。高效的意思是合sprintf系列一樣。調(diào)用的時候如下:
            fmt(text, "%s %s %d ", 20, 17.5, 'a'); //故意寫錯%s的,在這里,%s為通用符號
            fmt(file, "{%s-}",{1, 2, 3}); //輸出 1-2-3到文件中,也即是能夠fmt容器對象,橫線-為容器對元素的分隔符

            帶有切片功能的數(shù)組,此數(shù)組類型還支持子類型數(shù)組到基類型數(shù)組的隱式轉(zhuǎn)換,也即是需要用到基類型數(shù)組的參數(shù),子類型數(shù)組都可以適應(yīng)

            haskell的map,filter,fold算法在C++下的方便靈活組合性的改造,使用時,就好像C#的linq那么爽快,當(dāng)然,沒有l(wèi)ambda的參數(shù)自動推導(dǎo),畢竟還不如

            stackless協(xié)程

            c++下的monad

            wpf的依賴屬性在c++下的實現(xiàn),gui框架的不可缺少的要素

            tupple的功能擴展,通過宏,不需要寫類型,用起來就好像函數(shù)式語言原生的那么爽的可能

            好像haskell或者f#那樣的模式匹配的結(jié)構(gòu)體

            C++下完完全全實現(xiàn)狗語言的那種鴨子類型的接口

            面向?qū)ο蟮纳钊胩接懀瑢τ谄簌Z或者雞是一種鳥,繼承了鳥,但是沒有繼承了會飛的接口,在編譯期就能報錯,在運行期也不能對其找到會飛的接口

            具體類,基本類型,沒有虛函數(shù),但是又能實現(xiàn)接口的方式,是實實在在的接口,里面有純虛函數(shù),也即是非侵入式的實現(xiàn)接口,上面宇宙最強悍的fmt就是用到這里的技術(shù)

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

            類型安全的消息,一條消息就代表了一種函數(shù)調(diào)用,不是win32的那種一點也不安全的類型系統(tǒng),然后可以向任何類發(fā)送消息,動態(tài)添加消息的反應(yīng),消息隊列,消息和消息參數(shù)的保存,actor,command模式,redo或undo的輕松實現(xiàn),消息廣播

            空基類優(yōu)化的運用,除了多繼承(ATL)或者內(nèi)嵌類(MFC),還有其他方式,那是以組合方式,通過少量的模板和少量的宏,通過搭配組裝(多繼承空基類)各種基類,就能完成一個com組件

            消息系統(tǒng)的構(gòu)建,gui框架的編寫

            ........

            博大精深的c++!只是想說,上面的一切,在C++下全部都是可行的,當(dāng)然,宏,template,多繼承必須大用特用,只是,奇妙的是,主類的內(nèi)存布局卻很干凈,甚至可以沒有虛函數(shù)
            不知道有生之年能否填完坑,以之為勵吧!
            c++的同學(xué)們也充分發(fā)揮想象力吧,太多的奇技淫巧了。

            posted @ 2016-05-09 20:36 華夏之火 閱讀(1343) | 評論 (8)編輯 收藏

            僅列出標(biāo)題
            共5頁: 1 2 3 4 5 

            導(dǎo)航

            統(tǒng)計

            常用鏈接

            留言簿(6)

            隨筆分類

            隨筆檔案

            搜索

            積分與排名

            最新評論

            閱讀排行榜

            評論排行榜

            久久精品国产亚洲一区二区| 久久99久久无码毛片一区二区| 久久福利青草精品资源站| 蜜桃麻豆www久久国产精品| 久久精品国产亚洲av高清漫画 | 久久一本综合| AV狠狠色丁香婷婷综合久久 | 色欲综合久久躁天天躁蜜桃| 国产精品99久久久久久董美香| 久久丫忘忧草产品| 欧美日韩精品久久久久 | 无码AV中文字幕久久专区| 久久人人爽人人澡人人高潮AV | 久久国产免费观看精品| 国产成人精品综合久久久久| 久久人人爽人人爽人人片AV麻豆| 色成年激情久久综合| 久久久久亚洲av无码专区导航| 国产精品亚洲综合久久| 久久久久人妻精品一区三寸蜜桃| 99久久婷婷国产综合亚洲| 99久久无色码中文字幕人妻| 伊人精品久久久久7777| 无码人妻少妇久久中文字幕| 国产免费久久精品丫丫| 久久婷婷国产麻豆91天堂| 久久久精品一区二区三区| 国产精品久久久久天天影视| 国产成人精品久久一区二区三区 | 色综合久久久久| 久久久久免费精品国产| 久久综合综合久久狠狠狠97色88 | 三级韩国一区久久二区综合 | 久久午夜羞羞影院免费观看| 午夜不卡久久精品无码免费| 久久人妻无码中文字幕| 亚洲色大成网站www久久九| 亚洲午夜久久久久妓女影院| 欧美精品久久久久久久自慰| 久久久免费精品re6| 久久免费高清视频|