C++的面向?qū)ο笤O(shè)計(jì)能力,與java,C#這兩個(gè)雜碎相比,一直都是一個(gè)大笑話,現(xiàn)在誰(shuí)敢正兒八經(jīng)地用c++搞面向?qū)ο蟮目蚣芟到y(tǒng),業(yè)界都用java、C#搞設(shè)計(jì)模式,那關(guān)C++什么事情了。而C++也很有自知之明,很知趣,98年之后,就不怎么對(duì)外宣稱自己是面向?qū)ο蟮恼Z(yǔ)言,就不怎么搞面向?qū)ο笱芯苛耍y道是c++下的面向?qū)ο笠呀?jīng)被研究透徹?),一直在吃template的老本,一直到現(xiàn)在,template這筆豐厚的遺產(chǎn),貌似還夠c++吃上幾十年。今時(shí)今日,virtual早就淪落為template的附庸,除了幫助template搞點(diǎn)類(lèi)型擦除的行為藝術(shù)之外,就很難再見(jiàn)到其身影了。有那么幾年,業(yè)界反思c++的面向?qū)ο蠓妒剑?/span>virtual,特別是function出現(xiàn)之后,要搞動(dòng)態(tài)行為,就更加不關(guān)virtual的什么事情了。而那幾年,本座也學(xué)著大神忌諱virtual關(guān)鍵字。現(xiàn)在大家似乎已經(jīng)達(dá)成共識(shí),c++里頭的面向?qū)ο竽芰懿煌晟疲婷嫦驅(qū)ο缶蛻?yīng)該找其他語(yǔ)言,比如java、C#雜碎;或者更動(dòng)態(tài)類(lèi)型的語(yǔ)言,好比python,Ruby;或者干脆就是原教旨的面向?qū)ο螅ㄏl(fā)送),object C,smalltalk。
是啊,1、沒(méi)有垃圾回收;2、沒(méi)有原生支持的完善反射能力;3、多繼承、虛繼承導(dǎo)致的復(fù)雜內(nèi)存布局。這三座大山面前,c++的碼猿哪敢染指什么面向?qū)ο螅辉谄炔坏靡训那闆r下,小心翼翼地使用virtual。但是,事實(shí)上,要玩面向?qū)ο螅?/span>c++原來(lái)也可以玩得很炫,甚至,可以說(shuō),關(guān)于面向?qū)ο蟮哪芰Γ?/span>c++是最強(qiáng)的(沒(méi)有之一)。這怎么可能?
所謂的面向?qū)ο螅f(shuō)白了,就是對(duì)動(dòng)態(tài)行為的信息支持,能在面向?qū)ο笤O(shè)計(jì)上獨(dú)領(lǐng)風(fēng)騷的語(yǔ)言,都是有著完善的運(yùn)行時(shí)類(lèi)型信息,就連lisp,其運(yùn)行時(shí)元數(shù)據(jù)也都很完備。靜態(tài)強(qiáng)類(lèi)型語(yǔ)言(java、C#)與動(dòng)態(tài)語(yǔ)言比,顯然有著強(qiáng)大的靜態(tài)類(lèi)型能力(這不是廢話嗎),能在編譯期就提前發(fā)現(xiàn)類(lèi)型上的諸多錯(cuò)誤,但是也因此帶上靜態(tài)約束,導(dǎo)致呆板、繁瑣的代碼,java的繁文縟節(jié),就是最好證明;而動(dòng)態(tài)語(yǔ)言恰好相反,代碼簡(jiǎn)潔,廢話少,但是喪失靜態(tài)信息,所謂重構(gòu)火葬場(chǎng),那都是血和淚的教訓(xùn)。靜態(tài)語(yǔ)言與動(dòng)態(tài)語(yǔ)言真是一對(duì)冤家,如同光的波粒性,己之所長(zhǎng)恰是彼之所短,己之所短又是彼之所長(zhǎng),魚(yú)與熊掌不可兼得。而C++竟然能集兩家之所長(zhǎng),在靜態(tài)語(yǔ)言的領(lǐng)域中玩各種動(dòng)態(tài)行為藝術(shù),比如動(dòng)態(tài)修改類(lèi)型的反射信息,千奇百怪的花樣作死(喪心病狂的類(lèi)型轉(zhuǎn)換);在動(dòng)態(tài)范疇里面,又可以在編譯期榨取出來(lái)靜態(tài)類(lèi)型信息,比如,消息發(fā)送的參數(shù)信息,想想win32的無(wú)類(lèi)型的wparam和lparam,每次都要猿猴對(duì)照手冊(cè)解碼,從而最大限度地挖掘編譯器的最大潛力。所以說(shuō),c++是最強(qiáng)大的面向?qū)ο笳Z(yǔ)言,沒(méi)有之一。而把靜態(tài)和動(dòng)態(tài)融為一體之后,c++的抽象能力也到達(dá)一個(gè)全新的高度,自動(dòng)代碼生成,以后再發(fā)揮,這是一個(gè)龐大的課題。C++令人發(fā)指的強(qiáng)大,絕對(duì)遠(yuǎn)遠(yuǎn)超乎等閑猿猴的想象,特別是那批c with class的草覆蟲(chóng)原始生物。C++只在部分函數(shù)領(lǐng)域的概念上表現(xiàn)令人不滿,比如lambda表達(dá)式的參數(shù)類(lèi)型自動(dòng)推導(dǎo),monad表達(dá)式,缺乏原生的延遲求值等。當(dāng)然,c++整個(gè)的設(shè)計(jì)理念非常繁雜隨心所欲,但是,卻可以在這一塊混沌里面整理出來(lái)一些舉世無(wú)雙的思想體系,就是說(shuō),c++是一大堆原材料,還有很多廚房用具,包括柴火,讓猿猴自行下廚,做出來(lái)的菜肴可以很難吃,也可以是滿漢全席,全看猿猴的手藝。
當(dāng)然,要在c++里頭搞面向?qū)ο螅嗬^承,虛繼承的那一套,必須徹底拋棄。最大的問(wèn)題是,多繼承會(huì)導(dǎo)致混亂未知的二進(jìn)制內(nèi)存布局,虛函數(shù)表也一塌糊涂,十幾年前,c++設(shè)計(jì)新思維的基于policy的范式,雖然令人耳目一新,也因?yàn)檫@種范式下對(duì)象的內(nèi)存布局千奇百怪,所以,即便是最輕微的流行也沒(méi)有出現(xiàn)過(guò)。當(dāng)然,也不可能大規(guī)模搞消息發(fā)送這種很geek的套路,功能太泛化了,其實(shí),消息發(fā)送就是動(dòng)態(tài)的給對(duì)象添加成員函數(shù),并且可以在運(yùn)行時(shí)知道對(duì)象有多少成員函數(shù),那個(gè)成員函數(shù)可以對(duì)該消息做出反應(yīng),消息可以是字符串,整型ID(原子), MFC的消息映射表(BEGIN_MESSAGE_MAP,…)就是一個(gè)功能?chē)?yán)重縮水版的好例子,c++下支持消息映射的庫(kù),絕對(duì)可以比破mfc的那一套要好上千百倍,不管是性能、類(lèi)型安全、使用方便上。目前除了在gui這種變態(tài)的場(chǎng)合下才需要大搞消息發(fā)送,其他場(chǎng)景,完全可以說(shuō)用不上,雖然說(shuō)消息發(fā)送很強(qiáng)大很靈活,但也因?yàn)槠錃μ珔柡Γ炊由髦亍_@好比goto,好比指針,好比stl的迭代器,什么都能做的意思,就是什么都盡量不讓它做。
那么,c++下搞面向?qū)ο螅€有什么法寶可用呢?當(dāng)然,在此之前,我們先要直面內(nèi)存分配。內(nèi)存既是c++的安身立命之本,又是c++淪落為落水狗喪家犬之幕后大黑手。假如不能為所欲為的操作內(nèi)存,那么c++的折騰法子,奇技淫巧,起碼要死掉一大半以上。而由于要支持各種花樣作死的內(nèi)存操作,c++的垃圾回收遲遲未曾出現(xiàn),就連以巨硬之大能整出來(lái)的.net那么好的gc,霸王硬上弓,在給原生c++強(qiáng)硬加上托管功能(垃圾回收),都出力不討好。可見(jiàn)未來(lái)垃圾回收,對(duì)c++來(lái)說(shuō),嗯,想想就好了。內(nèi)存是資源,沒(méi)錯(cuò),用raii來(lái)管理,也無(wú)可厚非。但是,內(nèi)存卻是一種很特殊的資源,1、內(nèi)存時(shí)對(duì)象的安身立命之所;2、不同于普通資源,內(nèi)存很多,不需要馬上用完就急急忙忙啟動(dòng)清理工作,只要系統(tǒng)還有大把空余的內(nèi)存,就算還有很多被浪費(fèi)了的內(nèi)存,都不要緊,gc也是因?yàn)檫@個(gè)原因才得以存在。相比內(nèi)存,普通資源給人的感覺(jué)就是數(shù)量及其有限,然后要提交工作結(jié)果,否則之前所做努力就廢了。所以,對(duì)于內(nèi)存,應(yīng)該也要特別對(duì)待。就算raii,也要采用專門(mén)的raii 。
假設(shè)我們的程序里面使用多種內(nèi)存分配器,比如說(shuō),每個(gè)線程都有自己專有的內(nèi)存allocator對(duì)象,然后,線程之間的共享數(shù)據(jù)由全局的內(nèi)存分配器分配,線程的內(nèi)部對(duì)象都用線程的專屬allocator來(lái)分配,那么,內(nèi)存分配器就是一個(gè)線程局部變量(tls,thread local storage)。于是,可以規(guī)定,所有的內(nèi)存分配都通過(guò)GetTlsAllocator()來(lái)new對(duì)象,當(dāng)然,確定是全局共享變量的話,沒(méi)辦法,就只能用GetGlobalAllocator()來(lái)new對(duì)象。那么,有理由相信,啟動(dòng)一個(gè)任務(wù)時(shí),我們先定義一個(gè)arena allocator變量,并令其成為當(dāng)前線程的專屬內(nèi)存分配器,那么這個(gè)任務(wù)后面的所有new 出來(lái)的對(duì)象,包括循環(huán)引用,都不必關(guān)心。只要任務(wù)一結(jié)束,這個(gè)arena allocator變量一釋放,所有寄生在它身上的對(duì)象,全部也都消失得干干凈凈,沒(méi)有任何一點(diǎn)點(diǎn)的內(nèi)存泄露。就算任務(wù)內(nèi)部有大量的內(nèi)存泄露,那又如何,任務(wù)一結(jié)束,所有跟此任務(wù)有關(guān)的一切內(nèi)存,全部成塊清空。總之,不要以常規(guī)raii來(lái)解決內(nèi)存困境,解放思想,在內(nèi)存釋放上,我們可以有九種辦法讓它死,而不是僅僅靠shared_ptr,unique_ptr,weak_ptr這些狹隘的思維。
其次,完善的面向?qū)ο笤O(shè)計(jì),避免不了完備的反射,用以在運(yùn)行時(shí)提供動(dòng)態(tài)類(lèi)型信息,無(wú)參模板函數(shù)可以把靜態(tài)類(lèi)型映射成全局唯一變量,好比,TypeOf<vector<int>>,返回vector<int>的全局唯一的const TypeInfo*對(duì)象,這個(gè)對(duì)象包含了vector<int>的所有靜態(tài)類(lèi)型信息,可以這么說(shuō),在靜態(tài)類(lèi)型層面上vector<int>所能做的任何事情,比如定義一個(gè)vector<int>的變量,也即是創(chuàng)建對(duì)象;遍歷、添加元素、析構(gòu)、復(fù)制賦值、元素?cái)?shù)量等等一切操作,與vector<int>對(duì)應(yīng)的TypeInfo對(duì)象,統(tǒng)統(tǒng)都可以做到。所不同的是,vector<int>的靜態(tài)類(lèi)型代碼,只能用于vector<int>自身的情況(這樣子可放在源文件中),又或者是通過(guò)template,表現(xiàn)行為類(lèi)似于vector<int>的數(shù)據(jù)類(lèi)型(代碼必須在頭文件上)。而用TypeInfo*做的事情,全部都在運(yùn)行時(shí)發(fā)生,所有的靜態(tài)類(lèi)型信息,全部被帶到運(yùn)行時(shí)來(lái)做,所以這些代碼全部都可以處在源文件里面,甚至動(dòng)態(tài)庫(kù)里頭,只不過(guò)是TypeInfo*操作的對(duì)象是一個(gè)二進(jìn)制內(nèi)存布局和vector<int>一模一樣的內(nèi)存塊,可以通過(guò)強(qiáng)制類(lèi)型轉(zhuǎn)換,把運(yùn)行時(shí)的內(nèi)存塊轉(zhuǎn)換成靜態(tài)編譯時(shí)的vector<int>。其實(shí)這里的思想,就是想方設(shè)法將豐富多彩的靜態(tài)類(lèi)型信息無(wú)損的保存到運(yùn)行時(shí)中,讓編譯時(shí)能做的事情,運(yùn)行時(shí)也可以做。差別在于,一個(gè)是用靜態(tài)類(lèi)型信息來(lái)做事情,這里,任何一點(diǎn)點(diǎn)類(lèi)型上的錯(cuò)誤,都會(huì)讓編譯器很不高興;一個(gè)則是用動(dòng)態(tài)類(lèi)型信息來(lái)做事情,這里,顯然只能讓猿猴人肉編譯器。這里,可見(jiàn)動(dòng)態(tài)類(lèi)型信息和靜態(tài)類(lèi)型信息的表達(dá)能力是等價(jià)的,也即是同等重要性的意義,而靜態(tài)類(lèi)型信息的意義有多大,相信大家都知道。
那么,如何建立完備的反射信息,這個(gè)必須只能用宏來(lái)配合完成,外部工具生成的反射信息代碼,功能很不完備,另外,c#、java等的反射信息全部都是編譯器生成的,可定制性很差。我們需要的是一點(diǎn)都不遜色于靜態(tài)行為的動(dòng)態(tài)行為。所以,只有由自己自行管理反射,才能做到真正意義上的完備反射。必要時(shí),我們還可以在運(yùn)行時(shí)修改反射信息,從而動(dòng)態(tài)地增刪對(duì)象的行為方式,改變對(duì)象的面貌。看到這里,是否覺(jué)得很多的設(shè)計(jì)模式,在這里會(huì)有更清晰更簡(jiǎn)潔的表達(dá)方式呢,甚至,輕而易舉就可以出現(xiàn)新的設(shè)計(jì)模式。比如,以下定義對(duì)象反射信息的代碼。
在c++下,由于全局變量生命周期的隨意性(構(gòu)造函數(shù)調(diào)用順序不確定,析構(gòu)順序也不確定),大家都很忌諱其使用,雖然全局變量功能很強(qiáng)大,很多時(shí)候都避免不了。但是,標(biāo)準(zhǔn)上還是規(guī)定了全局變量的順序,所有的全局變量必須在main函數(shù)之前構(gòu)造完成,其析構(gòu)函數(shù)也只能在main函數(shù)結(jié)束后才調(diào)用。另外,函數(shù)的靜態(tài)變量必須在其第一次訪問(wèn)之前構(gòu)造完整。基于這兩點(diǎn),我們就可以在main函數(shù)之前構(gòu)建全部的反射信息,流程是這樣子,所有的類(lèi)型的反射對(duì)象都是以函數(shù)內(nèi)部的靜態(tài)指針變量存在,他們都通過(guò)調(diào)用GetStaticAllocator()的內(nèi)存分配器來(lái)創(chuàng)建,這樣子,提供反射信息的函數(shù),就避免了其內(nèi)部TypeInfo對(duì)象的析構(gòu)發(fā)生。最后,main結(jié)束后,由GetStaticAllocator()函數(shù)內(nèi)的內(nèi)存分配器的析構(gòu)函數(shù)統(tǒng)一釋放所有反射信息占用的內(nèi)存。最后,附上一個(gè)例子
struct Student
{
//ClassCat表示為Student的基類(lèi),為空類(lèi),所以Student可以繼承它,但是代碼上又不需要明確繼承它,非侵入式的基類(lèi)。
//ClassCat提供二進(jìn)制序列化操作,xml序列化,json序列化,數(shù)據(jù)庫(kù)序列化等操作
PPInlineClassTI(ClassCat, Student, ti)
{
PPReflAField(ti, name);
PPReflAField(ti, age);
PPReflAField(ti, sex, { kAttrXmlIgnore }); //表示不參與xml的序列化操作
}
AString name;
int age;
bool sex;
};
struct Config : Student
{
PPInlineClassTI(Student, Config, ti)
{
PPReflAField(ti, map);
}
HashMap<U8String, int> map;
};
下期的主角是非侵入式接口,徹底替換c++上的多繼承,功能遠(yuǎn)遠(yuǎn)好過(guò)C#、java雜碎的弱雞接口,更超越狗語(yǔ)言的不知所謂的非侵入式接口。如果僅僅是完備的反射信息,而缺乏非侵入式接口,在c++下搞面向?qū)ο螅鋵?shí)還是很痛苦的。但是,有了非侵入式接口之后,一切豁然開(kāi)朗。甚至可以說(shuō),感覺(jué)c++里面搞那么多玩意,都不過(guò)是為了給非侵入式接口造勢(shì)。然而非侵入式接口一直未曾正式誕生過(guò)。