關于C++泛型編程的一些雜感 劉未鵬(pongba) C++的羅浮宮(http://blog.csdn.net/pongba) 一些關于GP的思考或總結(jié),沒有太多的技術細節(jié),主要是一些思想上的闡釋。另外,文字比較亂,沒有細細整理,湊合吧;-)
關于GP,可以說我是對它有很復雜的感情的,其實GP這種東西最好是建立在無類型語言上面,就C++0X目前對GP的支持的趨勢來看,確實如此,auto/varadic templates這些特性的加入象征著C++ GP的形式正越來越轉(zhuǎn)向一種更純粹的泛性語法描述,表面上你幾乎不會看到任何類型的痕跡,只有語法以及語法背后蘊涵的語義,然而在C++里面有一個“最大的國情”,即支持所有這些的是一個堅實的大地——強類型系統(tǒng)。所有的泛化所有的模板代碼一旦實例化之后就落實到某一集特定的類型身上然后接受強類型系統(tǒng)的考驗;-)有點像波函數(shù)的塌縮——本來是具有無數(shù)可能的,一旦有了一個觀測者立即就塌縮成一個實體。在GP中,觀測者就是使用者,或者說使用者給出的一集模板實參;-)
話說回來,雖說GP最好是建立在無類型語言,像LISP/Scheme這種語言上面,但它在C++里面卻又確確實實的獲得了極大的成功,這也正符合BS在D&E里面的思想——現(xiàn)實總是需要折衷的,正應了中國的一句古話“識時務者為俊杰”。像LISP這樣“純粹”的語言到了現(xiàn)實應用當中往往是“應用范圍狹窄”的同義詞(不過用在教學和研究方面還是挺有意思的,雖然現(xiàn)在的主流FPL社區(qū)正在致力于將FPL應用到工業(yè)界去,但肯定還需要一段時間的;-))。BS說C++從來都不是一門為漂亮而設計的語言,C++的語言特性都是從實際出發(fā),實實在在的加進去的。另一個有趣的觀察是,非主流的語言特性在主流語言當中往往能夠得到很好的發(fā)揮,C++STL將FPL風格初步運用到算法當中,算是獲得了比較好的效果,至于一些更為純粹的C++ FPL如boost::lambda,boost::spirit::phoenix,boost::bind,boost::lambda::ll,fcpp等的運用則還處于摸索階段。C++里面一個成功且必要的FPL風格運用是boost::mpl庫里面的,由于C++ Metaprogramming并不支持side-effect(副作用),換句話說,在C++Metaprogramming當中,一切數(shù)據(jù)都是immutable的,所以像我們通常所見的for循環(huán)結(jié)構(gòu)就不復存在了,轉(zhuǎn)而成為遞歸結(jié)構(gòu)。后者是FPL的招牌式結(jié)構(gòu);-)
C++GP的一個招人唇舌的地方兒就是它的語法,由于C++本質(zhì)上是一門強類型語言,而且并沒有內(nèi)置的partial evaluation、currying以及high order functional programming的支持,另外C++里面的函數(shù)也并非first class的對象。這些都使得我們在編寫C++ FPL的庫或通常的代碼的時候感到處處掣肘,雖然利用一些“神奇”的技巧在C++里面是可以overcome這些缺陷的,但是語法,還是語法,有時候語法有點讓人不可接受,當然,像我這樣的熱愛者會鼓吹說“其實它的語法也不是那么差…;-)”。但畢竟跟LISP、haskell這樣的原生FPL比起來,C++ FPL的語法還是顯得有點生硬了,純粹的FPL能夠關注于表達代碼的邏輯,理想情況下你看不到“類型”這回事。所以有人說haskell的表達就像數(shù)學一樣簡潔優(yōu)美來著;-)但在C++當中你不得不受制于類型系統(tǒng)的束縛,有點像“枷鎖上的舞蹈”,呵呵;-)
不過C++GP當中有一點奇妙的是,雖然我們熟知的runtime programming當中你并沒有內(nèi)建的對partial evaluation的支持,但在Metaprogramming里面卻優(yōu)雅的存在著,例如一個元函數(shù)plus,你可以寫plus<_1,100>,這就是一個partial evaluation,David在他的《C++ Template Metaprogramming》里面把這個稱為”partial function application(部分函數(shù)應用)”。但是在runtime的場景下這是不可能的,譬如一個runtime函數(shù)plus,你可以寫plus(_1,100)嗎?顯然不可以。不過等一下,這種說法不夠精確,如果plus是一個“lambda aware”的functor的話,這還是可行的,實際上已經(jīng)有了這方面的完善的工作,語法是plus[_1,100],怪異吧,呵呵。但話說到底這只不過是二類公民而已,需要自己做大量工作,C++內(nèi)建的函數(shù)并沒有這個能力,例如對于:
int plus(int i,int j);
你根本不可能使用plus(_1,2)。當然你可以重載出一個lambda aware的plus版本使這成為可行的,但每次都要做這種重復勞動太浪費了;-)作為比較,為什么Metaprogramming具有這種能力呢?主要是因為以C++類模板為依托的C++元函數(shù)具有一個良好的FPL特性,即lazy evaluation(惰性求值),這種能力是C++內(nèi)建的函數(shù)所沒有的,對于一個內(nèi)建函數(shù)如plus來說,你寫plus(…)就等于是在寫它的返回值,也就是說,evaluation會立即進行。但對于一個元函數(shù)plus<>來說,你寫plus<…>,求值并不立即進行,而是要等到你為它加上::value的時候,即plus<…>::value,這才算完成了求值過程。換句話說,我們通常見到的函數(shù),其求值過程是跟傳參過程綁在一起完成的,求值就是傳參,傳參就是求值。但元函數(shù)則不同,你可以先傳它一組參數(shù),卻可以在任意時刻去取它的返回值;-)
這就致使了C++ Metaprogramming的FPL能力從本質(zhì)上是完備的和內(nèi)建的;-)盡管語法仍然還是有點“那什么”;)。
上邊廢話扯了一堆,下面是寫畢業(yè)論文的時候的一些東西,比較基本(如果你愿意,也可以稱為本質(zhì)^_^),因為怕老師看不懂(@_@),老鳥就不必往下看了哈;-)
從語言層面上來說,現(xiàn)代的編程語言為復用提供了三種主要的基本途徑。結(jié)構(gòu)化、面向?qū)ο螅?/span>OO)以及泛型(GP)。
結(jié)構(gòu)化程序設計當中,提供復用性的語言特性主要是函數(shù),在軟件工程當中,函數(shù)可以被當成黑箱,實現(xiàn)一個或一組相關的功能(functionality),而用戶不用關心函數(shù)內(nèi)部的具體實現(xiàn),只要負責將參數(shù)送入,然后接受返回值就可以了,C庫函數(shù)就是極好的例子。
但是結(jié)構(gòu)化程序設計有它本質(zhì)上的缺點,這個缺點主要體現(xiàn)在代碼的阻止上面,進而影響了可維護性。結(jié)構(gòu)化程序設計的一個主導思想就是著名的“程序=操作+數(shù)據(jù)”,這里操作其實就意味著函數(shù)。雖然該論斷一言道破了軟件開發(fā)或程序設計的本質(zhì),但真正落實到實際開發(fā)當中,在成本控制方面,開發(fā)者還需要更強大的手段。譬如,結(jié)構(gòu)化程序設計的一個嚴重問題就是,與一組數(shù)據(jù)相關的一組操作不能很好的被封裝到一塊去,例如,在C語 言里面,我們要表達一個動物以及該動物的行為,我們只能采用一個接口,外加一組函數(shù)來表示。這種松散的組織方式就造成了理解和維護上的困難。而且,由于沒 有類的機制,函數(shù)的名字只能通過加上其對應類型的名字作為前綴來避免名字沖突。這不但增加了出錯的機會,從某種程度上也增加了系統(tǒng)的混亂。所以說結(jié)構(gòu)化程 序雖然提供了過程/函數(shù)級別的復用,但是這種復用能力在當今軟件開發(fā)當中是遠遠不夠的。而且由于數(shù)據(jù)跟操作之間松散的組織方式,所以結(jié)構(gòu)化程序并不是很適合大型而復雜的應用開發(fā)。之所以以前的一些操作系統(tǒng),如UNIX/LINUX系列全是以C來編寫,個人覺得,主要跟一些歷史遺留因素有關,另一個因素是當時C++尚未發(fā)展得像今天這般成熟。至于效率方面,C++標準委員會提交的一則技術報告[TR]很直觀的表示出,C++中的類機制跟用C來實現(xiàn)類似的封裝能力不但效率不打折扣,甚至有過之而無不及。另外,一些大型的效率相關的應用使用C++來實現(xiàn)也正實現(xiàn)了這一點。譬如.NET整個的基層架構(gòu)全是C++編寫。而且開發(fā)大型的3D游戲,C++幾乎是唯一的選擇??梢娫谛史矫妫⒎窍裨S多人一貫以為的那樣,
而OO則提供了一種更為高層的抽象手段和復用機會,一個被良好OO化的系統(tǒng)中的大部分構(gòu)造(construct)都應該是對象,對象與對象之間原則上通過消息來溝通,但大多數(shù)現(xiàn)代語言基于效率的考慮仍然是通過對象成員方法的調(diào)用來模擬消息的發(fā)送,這雖然帶來了一定的耦合程度,但提高了效率,是一種合理的折衷(tradeoff)。此外,一個良好地抽象化的OO系統(tǒng)中的接口應該是相對穩(wěn)定的,所以耦合于接口的對象之間仍然能夠保持絕大部分的獨立性和自由度。OO復用的成功的例子非常之多,著名的如微軟的COM/DCOM、OMG的CORBA。其主要思想在于從對象層次上來封裝一集相關的操作(或數(shù)據(jù)),對象向外部提供一組接口,每個接口提供一組相關的功能,比起原始的函數(shù)封裝來說,OO中的對象不單具有概念上的清晰性,同時其功能性方面的內(nèi)聚性,相關性也為復用提供了更直觀友好的表達方式。而像COM和CORBA這種大型的OO框架則更能提供位置無關的代碼復用,乃至于抽象到了面向服務(Service Oriented)的層次,為更為強大的復用提供了契機。
面向?qū)ο螅?/span>OO)程序設計的主要特點
然而,傳統(tǒng)的OO實現(xiàn)有一個很大的弱點,即它是緊綁定/有限(bounded)的。舉個例子,橡樹(Oak)和蘋果樹(AppleTree)都是樹(Tree)的一種,現(xiàn)在有一個樹的集合(Set),需要對該集合排序,排序準則是基于樹的高度,很顯然,一個樹要想能夠加入這個有序集的話,就必須繼承自Tree類,這就是一種緊綁定,一旦Tree基類有了改動,所有依賴于它的樹都必須重新編譯或改動,當然,一個設計良好的抽象基類是不應該常常改動的,但無論如何本質(zhì)上的綁定是肯定存在的。而且,這個對該集合排序的算法只能被應用到樹身上,因為它也是依賴于Tree抽象基類的。從另一個角度來說,只有樹才能夠被該算法排序。很顯然的,人也具有高度,如果我們想要對一個Person Set進行同樣邏輯的排序,我們就得重寫該算法,這就意味著重復勞動,不但要付出編程心力,還可能隱藏著錯誤。當然,一個聰明的設計可能會對這種情況進行進一步的抽象,提取出一個所謂Comparable接口,所有能夠比較的東西都繼承自該接口。但這同樣是一條荊棘遍布的道路,不但依賴的問題仍然沒有消除(仍然依賴于Comparable,乃至于Comparable里面的方法簽名),而且還可能出現(xiàn)類型混亂,譬如一個人(Person)具有Comparable接口,而一頭大象(Elephant)也具有Comparable接口,那么對這個排序算法來說,它們就是可Compare的,這在現(xiàn)實當中可能是沒有任何意義的,很可能會導致運行期異常的拋出。這會帶來運行期的高昂代價。最關鍵的還是,這種做法強制每個Comparable的類型都必須實現(xiàn)Comparable接口,才能夠利用該排序算法。后面我們將會看到,泛型編程完全解決了這個問題。不過,OO的緊綁定也為它帶來了一個強大的優(yōu)勢,即二進制可復用性。二進制可復用性是一種強大的能力,一個最簡單的例子就是C的庫函數(shù),它們的實現(xiàn)全都是放在二進制庫當中的,用戶唯一可見到的就是函數(shù)的頭文件當中的聲明。本質(zhì)上,只要規(guī)定用戶遵從某個二進制約定,就可以實現(xiàn)二進制復用,而類的繼承,即OO的 實現(xiàn)機制,在大部分現(xiàn)代語言當中,本質(zhì)上就屬于一種二進制約定。派生類的虛函數(shù)表跟基類的虛函數(shù)表必須布局一致,這樣一來從二進制層面,派生類就能夠被當 作基類來使用了。當然,并非一定要犧牲松散耦合性才能夠獲取二進制復用性,換句話說,并非一定得使用類繼承才能獲得二進制復用性。目前之所以需要這么做, 是因為絕大部分的語言都是將類繼承機制建立在虛函數(shù)表之上,即二進制層面之上的。
但是,OO在效率方面卻顯示出了先天性的不足,前面已經(jīng)詳細解釋過,這種先天性不足是由于OO乃是建立在類繼承體系之上的一種思想(至少目前的主流OO實現(xiàn)莫不如是),而且在主流OO實現(xiàn)當中,出于效率上的考慮,對象之間的消息傳遞都是基于方法的調(diào)用,進一步增加了耦合程度。這就使得基于OO的泛性構(gòu)件只能夠被應用到有限的一集對象上。而且,由于OO的基于繼承的本質(zhì),實現(xiàn)泛性構(gòu)件必然要用到動態(tài)轉(zhuǎn)換,造成對于某些應用(如嵌入式系統(tǒng),軟實時系統(tǒng)乃是硬實時系統(tǒng))可能無法承受的負擔。這就是有名的abstraction penalty,意即抽象需要付出的代價。從另一個方面來說,也是從更本質(zhì)的方面來說,這是由于沒有將編譯期的類型信息足夠的利用起來。譬如說,JAVA(在沒有引入JG(Java Generic[JG])之前,所有的容器都是基于繼承的,容器中只保存Object引用,然而對于用戶來說,代碼中將某個容器使用在保存,譬如說int的時候,往后就肯定只能(且只應該)再往里面添加int,而不能是其它東西(之多是跟int兼容的對象,如char等)。但是對于Java來說,用戶完全可以往里面塞入一頭大象!換句話說,在這個方面,Java的語言并沒有為用戶提供一個強類型檢查的設施。這個設施應該能夠確保代碼結(jié)構(gòu)的前后一致性,某種程度上,這就是指類型。換一種更底層的描述就是,當用戶決定將一個List用于保存對象X的時候,編譯器/語言就應該能夠以某種形式來將X這個類型信息保存起來,在后續(xù)的代碼當中,如果用戶還想往這個List里面添加東西的時候,編譯器/語言應該能夠?qū)⑵漕愋透?/span>X對比,如果不兼容,那么就拒絕接受這樣的做法。然后在Java引入JG之前,一旦某個東西被扔進了容器,其靜態(tài)類型信息就會喪失殆盡,盡管其完整的類型信息仍然依附于對象上,可供運行期類型識別(RTTI)來操縱,但那是在運行期,彼時一旦出現(xiàn)錯誤只能亡羊補牢,不但損失效率,還喪失了及早提醒開發(fā)人員的良機。
二進制復用的問題:OO是基于二進制契約來進行復用的。這就限制了它的復用程度,譬如,一個排序算法如果要被復用的話,被排序的對象就必須實現(xiàn)諸如IComparable這 樣的接口。也就是說,必須遵從排序算法訂下的二進制契約。這意味著一種強制性,如果要復用該庫的算法或其他構(gòu)件的話,必須遵循它規(guī)定的一些接口規(guī)則,這就 將我們的構(gòu)件與該算法耦合起來了,現(xiàn)在如果有其他算法有一個更好的實現(xiàn),但需要我們實現(xiàn)另一個語義相同但名字不同的接口,譬如ICmp(IComparable的 縮寫)的話,我們除了改動我們的現(xiàn)存類之外就沒法讓我們的類適應于新的算法。而從算法的角度來說,一旦某個算法依賴上了某個接口,那么該算法就只能被應用 到實現(xiàn)了該接口的對象上了。很可能其他對象也實現(xiàn)了相應的語義,只不過是用其他接口來表達或者根本沒用接口來表達,譬如對于IComparable來說,人們可能僅用一個equal方法就表達了。雖然后者同樣具有“Comparable”的語義,但卻無法使用一個依賴與IComparable接口的算法來對它進行排序。某種意義上來說,這就限制了語義的表達。一個不完全的解決方式:當然,這個問題有一個解決方案,就是軟件工程當中經(jīng)典的解耦合手段——當A與B之間存在循環(huán)依賴時,應該進行接口分解,并讓A和B都依賴于一個更高層的接口。這里我們可以套用這種辦法,將IComparable接口提升到一個更高的層次,兩個算法都必須依賴于該接口,這樣一來就形成了一個公共契約,大家都必須遵從,一定程度上避免了某些構(gòu)件的自行其道。但是,這個解決方案仍然存在一個更嚴重的問題,即所有與IComparable相關的構(gòu)件依然全部都依賴與該接口。耦合仍然存在,關鍵是,人們在表達該接口所具有的語義時仍然需要實現(xiàn)該接口,而且所有與該接口相關的算法要想讓它自身能夠被廣泛復用的話,就必須遵從這個接口,而不能自行其道的定義一個類似ICmp的接口。這其實就是說,在OO里面,你如果想要實現(xiàn)某個接口所具有的語義、而且想讓你的類能夠得到最大程度的復用的話,你就必須將你的類耦合到某個二進制契約上。GP與源代碼復用:最好的解決方案是這個“更高的層次”是語言,即大家都遵從語言內(nèi)建的接口,譬如,所有語言當中都有四則運算以及比較(< >)操作,operator < 就是語言內(nèi)建的接口,讓所有需要“比較”語義的構(gòu)件都遵從語言這個內(nèi)建的操作是最為理想的方式。大家不會弄錯接口的名字,不會忘記實現(xiàn)接口,也不會導致互相之間的不兼容。STL的算法正是利用了這一點,所以得到了極高的復用性。而總的來說,GP提供的源碼級復用則是使用編譯時間來換取了松散耦合性。
泛型編程(GP)
C++還未標準化的時候,泛型編程的先驅(qū)Al Stevens(STL之父)當時就發(fā)現(xiàn)有些算法并不依賴于數(shù)據(jù)結(jié)構(gòu)的具體實現(xiàn),而只是依賴該結(jié)構(gòu)的幾個最基本的屬性。學過離散數(shù)學的人都知道,一個集合如果要能夠被排序的話,其上必須定義了“序”的概念所謂偏序集就是定義了偏序關系的集合。在C++里面,偏序關系就被映射成為了operator <,只要某個集合或區(qū)間內(nèi)的元素支持operator <,即存在偏序關系[1],那么這個集合就是可排序的,而不用去管其中保存的是猴子還是大象。這也就是說,我們的算法應該只依賴于這個最基本的屬性——偏序——就可以了。當然,OO也可以做到這一點,但效率令人無法容忍。GP解決了這個問題,使用GP來抽象偏序關系,可以完全消除abstraction penalty,效率跟手動寫出一個專用(specialized)的算法一樣高效。這完全要歸功于GP的理念。 GP與OO的比較
比OO好得多的通用性:OO的通用性是建立在類繼承體系之上的,這導致了算法只能夠被用在有限的類型上面。而GP的算法是建立在結(jié)構(gòu)一致性(Structural Conformance)上面,簡單的說,結(jié)構(gòu)一致性是指語法結(jié)構(gòu)上的一致性,GP假定某個特定的語法跟其語義能夠?qū)饋?。譬如說,std::sort()算法的一個版本只要求其操作數(shù)支持operator <,基本上這就是說,只要對于其操作數(shù)來說“a<b”能夠通過編譯,該算法就能成功。這不像OO,利用OO完成的通用算法要求其操作數(shù)繼承自某個特定的基類,如LessThanComparable。所以說,GP算法能夠運用到的類型集是無限的/非綁定的(unbounded)。
比OO好得多的效率:同樣,OO由于以類繼承為主流實現(xiàn)機制,而類繼承層次之間的轉(zhuǎn)換免不了會設計RTTI,這是沒有能夠充分利用編譯期靜態(tài)類型信息的結(jié)果。GP把這個能力引入了語言,一個std::list<int>里面保存的一定是int,而一個std::list<double>里面保存的則一定是double,換句話說,靜態(tài)類型信息被完整的保存在了編譯期。這就帶來了兩個好處,其一就是效率,由于知道對象確切的類型,所以編譯器進行代碼生成的時候就不用運用RTTI,這使得GP算法的效率跟手動編碼一樣高。一個sort<int>()算法跟你自己手寫一個專門針對int數(shù)組的sort()算法相比,效率不相上下,甚至前者更好一些。
比OO強得多的類型檢查: 靜態(tài)類型信息得以保存的另一個好處就是可以在編譯期發(fā)覺更多潛在的錯誤。一個被廣泛認可的觀點是強類型有助于程序的早期糾錯。因為良好的代碼當中的類型應 該是一致的,例如,假設你為長腿的東西定義“跑”這個行為,而實際上你要是把一個茶壺也拿來“跑”一下那肯定就是荒唐的。靜態(tài)類型檢查會為你查出這個錯 誤。當然,像JAVA這種語言當中也是能夠進行一定程度的靜態(tài)類型檢查的,但是其中類繼承抽象的廣泛運用無疑會極大程度上削弱這個機會。
GP的一個主要的弱點就是它不是二進制可復用的,這是因為支撐GP的抽象機制是語法一致性,而語法是代碼層面的東西,語法上的約定無法體現(xiàn)在二進制層面(至少目前不能)。相較之下主流OO實現(xiàn)中的約定天生就是二進制層面的。GP的這個弱點某種程度上阻礙了它成為一個首選的商業(yè)特性,因為以GP實現(xiàn)的庫,其源代碼基本上是必須公開的。而傳統(tǒng)的C庫全是以二進制形式發(fā)布的。不過GP由于其本質(zhì)上的強大能力,在代碼復用性,組織性,可維護性以及效率等方面表現(xiàn)出來的優(yōu)勢,很快就跨入了工業(yè)界,雖然等到其成熟運用還需一段時間,但從各門現(xiàn)代語言(以JAVA和C#為代表)紛紛加入對它的支持來看,這是大勢所趨!
meta-programming(元編程)
1994年,Erwin Unruh在C++標準委員會上遞呈了一個小程序,這個小程序后來被人們認為時C++模板元編程最早期的雛形。該程序能夠在編譯期打印出一列素數(shù),它們呈現(xiàn)在編譯錯誤信息當中。Todd Veldhuizen進而提出了模板元編程的概念。它把元程序(metaprogram)跟程序(program)區(qū)分了開來。元程序是一個操縱其他程序的程序,例如編譯器、部分求值器、解析器生成器等等。Erwin Unruh的素數(shù)計算模板展示了一種可能:你可以使用C++模板系統(tǒng)來寫出編譯期的程序。由此Todd Veldhuizen得到啟發(fā),可以使用C++模板來進行所謂的元編程,例如,通過在編譯期將各個不同的代碼片斷編織在一起從而構(gòu)成一個具有針對性的算法。
元 編程向來被認為是C++模板技術當中的最為高深的東西,但其實元編程跟普通的編程邏輯也是同樣的,應該說,只要是編程,不管是哪種編程,不管是哪種語言, 最重要的是邏輯。但雖說如此,軟件開發(fā)的重要方面仍然是不光包括邏輯的。成本也是另一個相當不容忽視的方面。元編程主要能夠帶來兩點好處:
u 早期(編譯期)的類型檢查:C ++的理念之一就是強類型,所謂強類型,從根本上其實是一種幫助及早發(fā)現(xiàn)錯誤的手段。強類型語言的設計者很早就發(fā)現(xiàn),許多程序的錯誤都會體現(xiàn)在類型的不一 致上面。譬如說,一個容器,如果人們想讓它裝A類對象,但后來又把B類對象裝了進去,那么這肯定是不對的,除非A是B的基類且該容器中存放的是引用(這里 指泛意的引用,并非C++的引用,在C++中引用是不能放在容器當中的[注])。C++模板元編程則把這種編譯期差錯的理念發(fā)揮到了及至,從思想上來講, C++元編程就是鼓勵盡早的把能夠在編譯期作出的決策放在編譯期,并且盡量在編譯期檢查能夠檢查的任何正確性。
u 提升編譯期決策在整個決策過程當中的地位。在很多的構(gòu)件當中,有相當一部分程度上的決策應該是放在編譯期的,而且應該在編譯期給出來的。譬如一個回調(diào)函數(shù)類boost::function,它所接受的函數(shù)的簽名(原型)應該在使用它的時候就給出,這個決策很明顯應該在編譯期,所以我們用一個模板參數(shù)傳遞給它:boost::function<int(double)> call_back;這 就創(chuàng)建了一個能夠接受所有類型為int(double)[注]的函數(shù)/仿函數(shù)的回調(diào)器。當然,這只是最淺層次的模板應用,元編程的強大力量在于對類型的操 縱能力,譬如當你將某個仿函數(shù)對象注冊到call_back回調(diào)器的時候,boost::function構(gòu)件會在它的內(nèi)部對你的仿函數(shù)的類型進行精確的 計算并確認出它是屬于哪一種類型的實例。再一個例子就是boost::variant,這個類實現(xiàn)union的強類型的版本,其中的元編程使用也達到了一 定的程度。譬如,我們想創(chuàng)建一個 int/double/X/Y四種類型的Union,我們這樣做boost::variant<int,double,X,Y> var;這時候你如果把一個不是這四種類型之一的類型的對象賦給var的話,你就會得到一個編譯期錯誤,阻止了未定義行為蔓延到運行期,這種神奇的技巧的 實現(xiàn)正得益于元編程的使用。
u 效率。 由于很多信息都放在了編譯期,進行計算和抉擇,所以元編程能夠顯著的提高構(gòu)件的效率,與不使用元編程的構(gòu)件相比,前者在編譯期做掉了許多工作,譬如類型轉(zhuǎn) 換,函數(shù)分派等。而后者則需要在運行期占用時間,甚至是顯著的時間,譬如在Java里面,使用一個List來存放對象,完了取出對象的時候必須從 Object引用轉(zhuǎn)變?yōu)閷ο蟮膶嶋H類型的引用,這里涉及到比較高的運行期開銷,雖然看上去只是一個c強制轉(zhuǎn)換,但背后隱藏著類型的比較,而這種比較卻完全 可以放在編譯期,根本不用消耗運行期寶貴的時間。當類層次結(jié)構(gòu)比較復雜的時候,或者涉及到類層次體系里面的橫向轉(zhuǎn)型(cross cast)的話,需要消耗的運行期時間更多。此外,在其他方面,元編程也提供了許多的編譯期機遇,譬如編譯期函數(shù)分派,或稱tag dispatching其實就是這樣一個例子??偟膩碚f,元編程的理念就是“能在編譯期做的事情就盡量在編譯期做掉”,這是像從JAVA——python——Lisp/Scheme(函數(shù)式編程語言)這樣的語言所不能做到的,因為它的語言里面缺乏相應的機制。C++某種程度上一切都是以效率為中心的,所以才有了模板和元編程。
2.9.1 強類型編程與弱類型編程的比較
舉 個最簡單的例子,如果你要使用一個容器的話,你肯定首先必須決定要用它來存放哪類對象,并且在你后續(xù)的使用當中自然必須遵從這個決策。這樣的容器一般叫做 同質(zhì)(homogeneous)容器,而另一類就是異質(zhì)(heterogeneous)容器,異質(zhì)容器一般在你不能確定某個容器在將來會存放什么樣的東西 時才會出現(xiàn),譬如你只知道這個容器以后會用來存放某些東西,至于這些東西的具體類型以后才會知道,這種情況在一個動態(tài)系統(tǒng)當中是經(jīng)常會遇到的情況。這種情 況下,如果是JAVA語言就非常容易,只需直接用List這樣的容器就行了,因為它們天生就是存放Object引用的。但JAVA在這個方面的類型強度還 不夠,不管是什么東西都可以且必須轉(zhuǎn)換為Object引用放在容器當中,從而一個同質(zhì)容器在外界看來其(編譯期)約束跟異質(zhì)容器沒什么差別,即用戶可以在 一個語義上為同質(zhì)的容器當中存放五花八門的類型的對象。
而 在C++當中,實現(xiàn)異質(zhì)容器需要更為復雜的技巧,因為總所周知C++的RTTI能力非常薄弱,基本上就是在動態(tài)類型身上掛了一個表示類型的字符串而已,這 個字符串被包在一個typeinfo結(jié)構(gòu)里面,后者卻還是不可移植的,不同平臺上完全可以而且實際上也確實對其進行不同的表示。所以這樣一來C++中要實 現(xiàn)異質(zhì)容器的話恐怕就涉及到一個根本的問題,元數(shù)據(jù)的存在與否。在小尺度上,這可以通過規(guī)定一組類型的描述以便取代不可移植的typeinfo結(jié)構(gòu)來實 現(xiàn),但這種方式是有限制的,只適用于一個庫,因為一個庫里面的類型一般是有限的一個集合,所以可以為它們規(guī)定一組描述方式,但用戶自己可能創(chuàng)建的類型卻是 無限的,庫根本沒法規(guī)定讓用戶也按照它們的思路去描述一個類型。這時就必須利用編譯器的權(quán)利了,只不過目前C++標準還沒有將typeinfo的二進制格 局標準化,恐怕這要等到C++中出現(xiàn)ABI的時候。
不 過,boost庫當中有一個設施:boost::any,它是創(chuàng)建異質(zhì)容器的絕佳選擇,但很遺憾,由于上面描述的typeinfo以及C++ ABI方面的緣故,它并不是可移植的。但如果是在同一個平臺之上或者只考慮源代碼可移植的話,其能力還是相當強大的。譬如你可以創(chuàng)建這樣一個容器:
std::vector<boost::any> va;
X x;
Y y;
va.push_back(x);
va.push_back(y);
這 就導致了x,y這兩個不同類型的元素可以被放入同一個容器當中。但后面取出元素的過程可就不那么直接了,對于這個例子而言,我們知道va.front() 返回的應當是一個X類型的元素,不過由于C++的強類型特性,它實際的返回類型是boost::any,我們可以這樣來“恢復”它原本的類型:
// just retrieve the value since this will lead to ‘return by value‘
boost::any_cast<X>(va.front());
// on the other hand, if you wanna modify the element in place
X* px = boost::any_cast<X>(&va.front());
*px = ...;
能夠這樣做的前提在于必須知道元素確切的類型。而且知道這些類型跟它們的對象之間的對應。否則轉(zhuǎn)換就面臨出錯的危險。這里我們可以通過把類型跟它的轉(zhuǎn)換函數(shù)放在一起的方式來將“元數(shù)據(jù)”加 到容器中的數(shù)據(jù)身上。也可以通過一些其他的技巧,具體看面臨的任務而定。譬如,boost::signal在存放各種具有相同的簽名的函數(shù)或仿函數(shù)的時候 就是采取的這種策略,它將用戶給定的函數(shù)或仿函數(shù)(普通函數(shù)跟仿函數(shù)的類型是不同的,函數(shù)可能具有兼容的簽名(例如int f(int)跟int g(double)就都是兼容于double (*)(int)的))從而類型各不相同,而仿函數(shù)則每個都不相同,因為它們是不同的類)包裝成同一類型的boost::function<... >對象,然后再將它存入std::map<boost::any>容器當中,當需要調(diào)用到它們的時候再將它們?nèi)〕鰜恚D(zhuǎn)型為 boost::function<...>,然后進行調(diào)用,由于boost::function實際存儲了它接受的每個(仿)函數(shù)的真正類 型,所以把真正存放在底部的那些函數(shù)/仿函數(shù)(先前由用戶注冊進去的)恢復原來類型就是boost::function的任務了。這種“數(shù)據(jù)+轉(zhuǎn)型操作實例”的捆綁是C++彌補其RTTI能力弱勢的一個慣用手段。
這
樣以來,強弱類型的優(yōu)劣就很明顯了,典型的強類型語言如C++能充分利用強類型的編譯期糾錯能力,同時也能夠一定程度上的實現(xiàn)弱類型語言的一些動態(tài)能力,
雖然復雜一點,但并非本質(zhì)上的缺陷。而弱類型的語言就不能反過來利用編譯期信息了,這是一種很可惜的浪費。后面我們會看到,元編程不光是具有在編譯期發(fā)覺
錯誤的能力,還能夠“強迫”用戶把應該在編譯期進行的決策提前到編譯期,從而從另一個方面更有利于了編譯期的糾錯。應該說兩者是密不可分的。“后者是前者的前提和促進,前者是后者的保障”;-)。
[1] 嚴格的說,這被稱為嚴格偏序(Strict Partial Ordering)或嚴格弱序(Strict Weak Ordering)。所謂嚴格偏/弱序即是指 |
|