GP技術(shù)的展望——C--
莫華楓
C++的復(fù)雜是公認(rèn)的,盡管我認(rèn)為在人類(lèi)的聰明智慧之下,這點(diǎn)復(fù)雜壓根兒算不上什么。不過(guò)我得承認(rèn),對(duì)于一般的應(yīng)用而言,C++對(duì)程序員產(chǎn)生的壓力還是不
小的。畢竟現(xiàn)在有更多更合適的選擇。僅僅承認(rèn)復(fù)雜,這沒(méi)有什么意義。我不時(shí)地產(chǎn)生一個(gè)念頭:有什么辦法既保留C++的優(yōu)點(diǎn),而消除它的缺點(diǎn)和復(fù)雜。我知道
D語(yǔ)言在做這樣的事情。但是,D更多地是在就事論事地消除C++的缺陷,而沒(méi)有在根本上消除缺陷和復(fù)雜性。
一般而言,一樣?xùn)|西復(fù)雜了,基本上都是因?yàn)闁|西太多。很顯然,C++的語(yǔ)言特性在眾多語(yǔ)言中是數(shù)一數(shù)二的。于是,我便想到或許把C++變成“C--”,可以治好C++的復(fù)雜之病。在探討這個(gè)問(wèn)題之前,讓我們先從C出發(fā),看看C++為何如此復(fù)雜。
C和C++
盡管存在這樣那樣的不足,比如non-lalr的語(yǔ)法、隱式的指針類(lèi)型轉(zhuǎn)換等,但C語(yǔ)言的設(shè)計(jì)哲學(xué)卻是足夠經(jīng)典的。C語(yǔ)言有一個(gè)非正式的分類(lèi),認(rèn)為它既非
匯編這樣的
低級(jí)語(yǔ)言,也非
Pascal那樣的
高級(jí)語(yǔ)言,
而應(yīng)該算作中級(jí)語(yǔ)言,介于其他兩類(lèi)語(yǔ)言之間。這種分類(lèi)恰如其分地點(diǎn)出了C語(yǔ)言的特點(diǎn)和理念:以高級(jí)語(yǔ)言語(yǔ)法形式承載了低級(jí)語(yǔ)言的編程模型。低級(jí)語(yǔ)言的特點(diǎn)
是可以直接地描述硬件系統(tǒng)的結(jié)構(gòu)。C則繼承了這個(gè)特點(diǎn)。C語(yǔ)言直觀地反映了硬件的邏輯構(gòu)造,比如數(shù)組是內(nèi)存塊,可以等價(jià)于指針。在C語(yǔ)言中,我們可以幾乎
直接看到硬件的構(gòu)造,并且加以操作。這些特性對(duì)于底層開(kāi)發(fā)至關(guān)重要。
然而,C的這種直觀簡(jiǎn)潔的模型過(guò)于底層和瑣碎,不利于應(yīng)用在那些構(gòu)造復(fù)雜、變化多樣的應(yīng)用中。針對(duì)C的這些弱點(diǎn),Bjarne
Stroustrup決心利用OOP技術(shù)對(duì)C語(yǔ)言進(jìn)行改造,從而促使了C++的誕生。C++全面(幾乎100%)地兼容C,試圖以此在不損失C語(yǔ)言的直觀
和簡(jiǎn)潔的情況下,同時(shí)具備更強(qiáng)的軟件工程特性,使其具備開(kāi)發(fā)大型復(fù)雜系統(tǒng)的優(yōu)勢(shì)。這個(gè)目標(biāo)“幾乎”達(dá)到了,但是代價(jià)頗為可觀。
在經(jīng)歷了80、90年代的輝煌之后,C++的應(yīng)用領(lǐng)域開(kāi)始退步。一方面,在底層應(yīng)用方面,C++的很多特性被認(rèn)為是多余的。如果不使用這些特性,那么
C++則同C沒(méi)有什么差別。相反這些特性的使用,對(duì)開(kāi)發(fā)團(tuán)隊(duì)的整體能力提出了更高的要求。因而,在最底層,很多人放棄了C++而回歸了C,因?yàn)槟切└呒?jí)特
性并未帶來(lái)很多幫助,反而產(chǎn)生了很多負(fù)擔(dān)。另一方面,在高層開(kāi)發(fā)中,業(yè)務(wù)邏輯和界面也無(wú)需那么多底層的特性和苛刻的性能要求,更多簡(jiǎn)單方便、上手容易的語(yǔ)
言相比C++更加適合。C++的應(yīng)用被壓縮在中間層,隨著業(yè)界系統(tǒng)級(jí)開(kāi)發(fā)的不斷專(zhuān)業(yè)化,C++的使用規(guī)模也會(huì)越來(lái)越小。(當(dāng)然,它所開(kāi)發(fā)的應(yīng)用往往都是關(guān)
鍵性的,并且是沒(méi)有選擇余地的)。實(shí)際上,C++在這個(gè)層面也并非完美的工具。目前無(wú)法取代是因?yàn)闆](méi)有相同級(jí)別的替代品。D或許是個(gè)強(qiáng)有力的競(jìng)爭(zhēng)者,但一
方面出于歷史遺留代碼的規(guī)模和應(yīng)用慣性,另一方面D也并未完全解決C++面臨的復(fù)雜性問(wèn)題,D也很難在可見(jiàn)的將來(lái)取代C++。
實(shí)際上,C++的這種尷尬地位有著更深層次的原因。C++的本意是在保留C的底層特性基礎(chǔ)上,增加更好的軟件工程特性。但是,C++事實(shí)上并未真正意義上
地保留C的底層特性。回顧一下C的設(shè)計(jì)理念——直觀而簡(jiǎn)潔地反映底層硬件的特性。C++通過(guò)兼容C獲得了這種能力。但是這里有個(gè)問(wèn)題,如果我要獲得C的這
種簡(jiǎn)單直觀性,就必須放棄C++中的很多高級(jí)特性。這里最明顯的一個(gè)例子便是pod(
plain old data)。
在C中壓根沒(méi)有pod的概念,因?yàn)樗械膶?duì)象都是pod。但是,C++中有了pod。因?yàn)镃++的對(duì)象可能不是一個(gè)pod,那么我們便無(wú)法象在C當(dāng)中那樣
獲得直觀簡(jiǎn)潔的內(nèi)存模型。對(duì)于pod,我們可以通過(guò)對(duì)象的基地址和數(shù)據(jù)成員的偏移量獲得數(shù)據(jù)成員的地址,或者反過(guò)來(lái)。但在非pod對(duì)象中,卻無(wú)法這么做。
因?yàn)镃++的標(biāo)準(zhǔn)并未對(duì)非pod對(duì)象的內(nèi)存布局作出定義,因而對(duì)于不同的編譯器,對(duì)象布局是不同的。而在C中,僅僅會(huì)因?yàn)榈讓佑布到y(tǒng)的差異而影響對(duì)象布
局。
這個(gè)問(wèn)題通常并不顯而易見(jiàn)。但在很多情況下為我們制造了不小的障礙。比如,對(duì)象的序列化:我們?cè)噲D將一個(gè)對(duì)象以二進(jìn)制流的形式保存在磁盤(pán)中、數(shù)據(jù)庫(kù)中,或
者在網(wǎng)上傳輸,如果是pod,則直接將對(duì)象從基地址開(kāi)始,按對(duì)象的大小復(fù)制出來(lái),或傳輸,或存儲(chǔ),非常方便。但如果是非pod,由于對(duì)象的不同部分可能存
在于不同的地方,因而無(wú)法直接復(fù)制,只能通過(guò)手工加入序列化操作代碼,侵入式地讀取對(duì)象數(shù)據(jù)。(這個(gè)問(wèn)題不僅僅存在于C++,其他語(yǔ)言,如java、C#
等都存在。只是它們沒(méi)有很強(qiáng)烈的性能要求,可以使用諸如reflect等手段加以處理)。同樣的問(wèn)題也存在于諸如hash值的計(jì)算等方面。這對(duì)很多開(kāi)發(fā)工
作造成不小的影響,不僅僅在底層,也包括很多高層的應(yīng)用。
究其原因,C++僅僅試圖通過(guò)機(jī)械地將C的底層特性和OOP等高層特性混合在一起,意圖達(dá)到兩方兼顧的目的。但是,事與愿違,
OOP的
引入實(shí)際上使得C的編程模型和其他更高級(jí)的抽象模型無(wú)法兼容。在使用C++的過(guò)程中,要么只使用C的特性,而無(wú)法獲得代碼抽象和安全性方面的好處,要么放
棄C的直觀簡(jiǎn)潔,而獲得高層次的抽象能力。反而,由于C和OOP編程模型之間的矛盾,大大增加了語(yǔ)言的復(fù)雜性和缺陷數(shù)。
舍棄
但是,我們可以看到在C++中,并非所有的高級(jí)特性都與C的底層特性相沖突。很多使用C而不喜歡C++的人都表示過(guò)他們?cè)饨邮?a title="OB" id="tl28">OB,也就是僅僅使用
封裝 。對(duì)于
RAII,基本上也持肯定的態(tài)度。或許也會(huì)接受
繼承,但也表露出對(duì)這種技術(shù)帶來(lái)的復(fù)雜性的擔(dān)心。
動(dòng)多態(tài)是明顯受到排斥的技術(shù)。顯然這是因?yàn)閯?dòng)多態(tài)破壞了C的編程模型,使得很多本來(lái)簡(jiǎn)單的問(wèn)題復(fù)雜化。不是它不好,或者沒(méi)用,是它打破了太多的東西。
因而,我們?cè)O(shè)想一下,如果我們?nèi)コ齽?dòng)多態(tài)特性,那么是否會(huì)消除這類(lèi)問(wèn)題呢?我們一步步看。
動(dòng)多態(tài)的一個(gè)基本支撐技術(shù)是
虛函數(shù)。在使用虛函數(shù)的情況下,類(lèi)的每一次繼承都會(huì)產(chǎn)生一個(gè)
虛函數(shù)表(vtable),其中存放的是指向虛函數(shù)的指針。這些虛函數(shù)表必須存放在對(duì)象體中,也就是和對(duì)象的數(shù)據(jù)存放在一起(至少要關(guān)聯(lián)在一起)。因而,對(duì)象在內(nèi)存里并不是以連續(xù)的方式存放,而被分割成不同的部分,甚至身首異處(詳見(jiàn)《
Inside C++ Object Model》)。這便造成了前面所說(shuō)的非pod麻煩。一旦放棄虛函數(shù)和vtable,對(duì)象的內(nèi)存布局中,便不會(huì)有東西將對(duì)象分割開(kāi)。所有的對(duì)象的數(shù)據(jù)存儲(chǔ)都是連續(xù)的,因而都是pod。在這一點(diǎn)上,通過(guò)去除vtable,使得語(yǔ)言回歸了C的直觀和簡(jiǎn)單。
動(dòng)多態(tài)的內(nèi)容當(dāng)然不僅僅是一個(gè)虛函數(shù),另一個(gè)重要的基石是繼承。當(dāng)然,我們并不打算放棄繼承,因?yàn)樗⒉恢苯悠茐腃的直觀性和簡(jiǎn)潔性。不同于虛函數(shù),繼承
不是完全為了動(dòng)多態(tài)而生的。繼承最初的用途在于代碼復(fù)用。當(dāng)它被賦予了多態(tài)含義后,才會(huì)成為動(dòng)多態(tài)的基礎(chǔ)。以下的代碼可以有兩種不同的解讀:
class B : public A {};
從
代碼復(fù)用的角度來(lái)看,B繼承自A,表示我打算讓B復(fù)用A的所有代碼,并且增加其他功能。而從多態(tài)的角度來(lái)看,B是一個(gè)A的擴(kuò)展,B和A之間存在
is-a的
關(guān)系。(B是一個(gè)A)。兩者是站在不同層面看待同一個(gè)問(wèn)題。代碼復(fù)用,代表了編碼的觀點(diǎn),而多態(tài),則代表了業(yè)務(wù)邏輯的觀點(diǎn)。但是,兩者并非實(shí)質(zhì)上的一回
事。在很多情況下,基類(lèi)往往作為繼承類(lèi)的某種代表,或者接口,這在編碼角度來(lái)看并沒(méi)有對(duì)等的理解。而這種接口作用,則是動(dòng)多態(tài)的基礎(chǔ)。動(dòng)多態(tài)通過(guò)不同的類(lèi)
繼承自同一個(gè)基類(lèi),使它們擁有共同的接口,從而可以使用統(tǒng)一的形式加以操作。作為一個(gè)極端,
interface(或者說(shuō)
抽象基類(lèi)),僅僅擁有接口函數(shù)(即vtable)而不包含任何數(shù)據(jù)成員。這是純粹的接口。
然而,這里存在一個(gè)缺陷。一個(gè)接口所代表的是一組類(lèi),它將成為這一組類(lèi)同外界交互的共同界面。但是,使用基類(lèi)、或者抽象基類(lèi)作為接口,實(shí)質(zhì)上是在使用一個(gè)
類(lèi)型來(lái)代表一組類(lèi)型。打個(gè)比方,一群人湊在一起出去旅游,我們稱(chēng)他們這群人為“旅行團(tuán)”。我們知道旅行團(tuán)不是一個(gè)人,而是一個(gè)不同于“人”的概念。動(dòng)多態(tài)
里的接口相當(dāng)于把一個(gè)旅行團(tuán)當(dāng)作一個(gè)人來(lái)看待。盡管這只是邏輯上的,或許一個(gè)旅行團(tuán)的很多行為和一個(gè)人頗為相似。但是根本上而言,兩者畢竟不是相同層次的
概念。這樣的處理方法往往會(huì)帶來(lái)了很多弊端。
為了使繼承被賦予的這重作用發(fā)揮作用,還需要一項(xiàng)非常關(guān)鍵的處理:類(lèi)型轉(zhuǎn)換。請(qǐng)看以下代碼:
void func(A* a);
B b;
func(&b);
最后這行代碼施行了動(dòng)多態(tài),如果B
override了A的虛函數(shù)的話(huà)。很顯然,如果嚴(yán)格地從強(qiáng)類(lèi)型角度而言,&b是不應(yīng)當(dāng)作為func的實(shí)參,因?yàn)閮烧哳?lèi)型不匹配。但是如果拒絕接
受&b作為實(shí)參,那么動(dòng)多態(tài)將無(wú)法進(jìn)行下去。因此,我們放寬了類(lèi)型轉(zhuǎn)換的限制:允許繼承類(lèi)對(duì)象的引用或指針隱式地轉(zhuǎn)換成基類(lèi)的引用或指針。這樣,
形如func(&b);便可以順理成章地成為合法的代碼。
然而,這也是有代價(jià)的:
B ba[5];
func(ba);
后面這行函數(shù)調(diào)用實(shí)際上是一個(gè)極其危險(xiǎn)的錯(cuò)誤。假設(shè)在func()中,將形參a作為一個(gè)類(lèi)型A的數(shù)組對(duì)待,那么當(dāng)我們使用ba作為實(shí)參調(diào)用func()的
時(shí)候,會(huì)將ba作為A的
數(shù)組處理。我們知道,數(shù)組內(nèi)部元素是緊挨著的,第二個(gè)元素的位置是第一個(gè)元素的基址加上元素的尺寸,以此類(lèi)推。如果傳遞進(jìn)來(lái)的對(duì)象數(shù)組是B類(lèi)型的,而被作
為A類(lèi)型處理,那么兩者的元素位置將可能不同步。盡管B繼承自A,但是B的尺寸很有可能大于A,那么從第二個(gè)元素起,a[1]的地址并非ba[1]的地
址。于是,當(dāng)我們以a[1]訪(fǎng)問(wèn)ba時(shí),實(shí)際上很可能在ba[0]的內(nèi)部某個(gè)位置讀取,而func()的代碼還以為是在操作ba[1]。這便是C++中的
一個(gè)重要的陷阱——對(duì)象切割。這種錯(cuò)誤相當(dāng)隱蔽,危險(xiǎn)性極大。
由于C++試圖保留C的編程模型,因而保留了指針-數(shù)組的等價(jià)性。這種等價(jià)性體現(xiàn)了數(shù)組的本質(zhì)。這在C中是一項(xiàng)利器,并無(wú)任何問(wèn)題。但在C++中,由于存
在了繼承,以及繼承類(lèi)的隱式類(lèi)型轉(zhuǎn)換,使得這種原本滋補(bǔ)的特性成為了一劑毒藥。換句話(huà)說(shuō),C++所引入的動(dòng)多態(tài)破壞了C的直觀性。
舍棄之后
從上面的分析來(lái)看,動(dòng)多態(tài)同C的編程模型是不相容的。因而如果希望得到C的直觀性,并且消除C++的缺陷,必須放棄動(dòng)多態(tài)這個(gè)特性。現(xiàn)在來(lái)看看放棄之后將會(huì)怎樣。
一旦放棄了動(dòng)多態(tài),也就放棄了虛函數(shù)和vtable。此時(shí),所有的對(duì)象都是pod了。那么首當(dāng)其沖的好處,就是可以進(jìn)行非侵入的序列化、hash計(jì)算等等
操作。由于對(duì)象肯定是連續(xù)分布的,可以直接地將對(duì)象取出進(jìn)行編碼、存儲(chǔ)、計(jì)算和傳輸,而無(wú)需了解對(duì)象內(nèi)部的數(shù)據(jù)結(jié)構(gòu)和含義。另外一個(gè)重要的問(wèn)題也會(huì)得到解
決,這就是ABI。在C中統(tǒng)一的ABI很自然地存在于語(yǔ)言中。我們可以很容易地用link將兩個(gè)不同編譯器編譯的模塊連接起來(lái),而不會(huì)發(fā)生問(wèn)題。但
是,C++中做不到,除非不再使用類(lèi)而使用純C。目前C++還沒(méi)有統(tǒng)一的ABI,即便標(biāo)準(zhǔn)委員會(huì)有意建立這樣的規(guī)范,實(shí)現(xiàn)起來(lái)也絕非易事。但是,如果放棄
動(dòng)多態(tài)之后,對(duì)象的布局便回歸到C的形態(tài),從而使得ABI不再成為一個(gè)問(wèn)題。
另一方面,隨著動(dòng)多態(tài)的取消,那么繼承的作用被僅僅局限于代碼復(fù)用,不再具有構(gòu)造接口的作用。我們前面已經(jīng)看到,繼承類(lèi)向基類(lèi)的隱式轉(zhuǎn)換,是為了使基類(lèi)能
夠順利地成為繼承類(lèi)的接口。既然放棄了動(dòng)多態(tài),那么也就無(wú)需基類(lèi)再承擔(dān)接口的任務(wù)。那么由繼承類(lèi)向基類(lèi)的隱式類(lèi)型轉(zhuǎn)換也可以被禁止:
void func(A* a);
B b;
func(&b); //編譯錯(cuò)誤,類(lèi)型不匹配
進(jìn)而對(duì)象切割也不會(huì)發(fā)生:
B ba[5];
func(ba); //編譯錯(cuò)誤,類(lèi)型不匹配
盡管根據(jù)數(shù)組-指針的等價(jià)性,ba可以被隱式地轉(zhuǎn)換為B*,但是B*不再能夠隱式地轉(zhuǎn)換為A*,從而避免了對(duì)象的切割。
問(wèn)題是,如此簡(jiǎn)單地將動(dòng)多態(tài)放棄掉,就如同將水和孩子一起潑掉那樣,實(shí)際上放棄了動(dòng)多態(tài)帶來(lái)的好處。實(shí)際上并非如此。我們放棄動(dòng)多態(tài)這個(gè)特性,但并不打算放棄它所具有的功能,而是用另一種技術(shù)加以替代。這便是runtime concept(
這里和
這里)。
不同于以類(lèi)型為基礎(chǔ)的interface,
concept是獨(dú)立于類(lèi)型的系統(tǒng)。concept生來(lái)便是為了描述一組類(lèi)型,因而是接口最理想的實(shí)現(xiàn)手段。當(dāng)concept runtime化之后,便具有了與動(dòng)多態(tài)相同的功能(很多方面還有所超越)。
runtime
concept同樣需要類(lèi)似vtable的函數(shù)分派表,但由于它不是類(lèi)型,這些分派表無(wú)需存放在對(duì)象內(nèi)部,可以獨(dú)立放置(可以同RTTI信息放在一起),
并且只需一份。正是基于這個(gè)特性,方才保證了所有對(duì)象依然是pod,依然能夠保證對(duì)象布局的直觀性。
同樣,runtime concept承擔(dān)了接口的任務(wù),但不象動(dòng)多態(tài)那樣依賴(lài)于繼承和相應(yīng)的隱式類(lèi)型轉(zhuǎn)換。(通過(guò)自動(dòng)或手動(dòng)的concept_map)。因而,我們依舊可以禁止基于繼承關(guān)系的隱式類(lèi)型轉(zhuǎn)換,從而防止對(duì)象切割的情況。
一旦使用concept作為多態(tài)的實(shí)現(xiàn)手段,反倒促使原本動(dòng)多態(tài)的一些麻煩得到消除。在動(dòng)多態(tài)中,必須指定virtual函數(shù)。如此,在一個(gè)類(lèi)中會(huì)存在兩
種不同形態(tài)的函數(shù),實(shí)現(xiàn)動(dòng)多態(tài)的虛函數(shù),和無(wú)此功能的普通函數(shù)。準(zhǔn)確地維護(hù)這樣兩種函數(shù),頗有些難度。而且,函數(shù)是虛還是不虛,牽涉到系統(tǒng)的設(shè)計(jì),必須在
最初構(gòu)建時(shí)確定,否則以后很難修改。但在放棄動(dòng)多態(tài),使用concept的情況下,只要一個(gè)繼承類(lèi)中,使用相同的簽名覆蓋基類(lèi)中的函數(shù),便實(shí)現(xiàn)了多態(tài)。當(dāng)
進(jìn)行concept_map,即將接口與類(lèi)綁定時(shí),只會(huì)考慮繼承類(lèi)的函數(shù),而忽略基類(lèi)中被覆蓋的函數(shù)。于是,只需簡(jiǎn)單的覆蓋,便實(shí)現(xiàn)了多態(tài)的控制。對(duì)于是
否多態(tài)一個(gè)函數(shù),即是否改變基類(lèi)函數(shù)的行為,完全由繼承類(lèi)控制,在創(chuàng)建基類(lèi)時(shí)不必為此傷神。其結(jié)果就是,我們無(wú)需在系統(tǒng)設(shè)計(jì)的最初一刻就操心多態(tài)的問(wèn)題,
而只需根據(jù)實(shí)現(xiàn)的需要隨時(shí)實(shí)現(xiàn)。
其他
存在大量隱式轉(zhuǎn)換也是C++常受人詬病的一個(gè)方面,(特別是那些Pascal系的程序員)。隱式轉(zhuǎn)換的目的是帶來(lái)方便,使得編碼更加簡(jiǎn)潔,減少冗余。同時(shí)也使得一些技巧得以施行。但是,隱式轉(zhuǎn)換的副作用也頗為可觀。比如:
void fun(short a);
long a=1248;
fun(a); //頂多一個(gè)警告
這種轉(zhuǎn)換存在兩面性:一方面,它可能是合理的,因?yàn)楸M管a類(lèi)型long大于short,但很可能存放著short可容納的數(shù)值;但另一方面,a的確存在short無(wú)法容納的可能性,這便會(huì)造成一個(gè)非常隱蔽的bug。
C/C++對(duì)此的策略是把問(wèn)題扔給程序員處理,如果有bug那是程序員的問(wèn)題。這也算得上合情合理,畢竟有所得必有所失,也符合C/C++的一貫理念。但
終究不是最理想的方式。但是如果象Pascal那樣將類(lèi)型管得很死,那么語(yǔ)言又會(huì)失去靈活性,使得開(kāi)發(fā)的復(fù)雜性增加。
如果試圖禁止隱式類(lèi)型轉(zhuǎn)換,那么為了維持函數(shù)使用代碼的簡(jiǎn)潔性,函數(shù)必須對(duì)所有的類(lèi)型執(zhí)行重載。這大大增加了函數(shù)實(shí)現(xiàn)的負(fù)擔(dān),并且重復(fù)的代碼嚴(yán)重違背了
DRY原則。
現(xiàn)在或許存在一些途徑,使得在維持絕對(duì)強(qiáng)類(lèi)型的情況下獲得所希望的靈活性。鑰匙可能依然在concept手上。考慮如下的代碼:
void fun(Integers a);
long a=1248;
fun(a);
longlong b=7243218743012;
fun(b);
此處,fun()是一個(gè)函數(shù),它的形參是一個(gè)concept,代表了所有的整型。這樣,這個(gè)函數(shù)便可以接受任何一種整型(或者具有整型行為的類(lèi)型)。我們
相信,在一般的應(yīng)用下,任何整數(shù)都有完全相同的行為。因此,我們便可以很方便地使用Integers這個(gè)接口執(zhí)行對(duì)整數(shù)的操作,而無(wú)需關(guān)心到底是什么樣的
整數(shù)。
如此,我們便可以在禁止隱式類(lèi)型轉(zhuǎn)換,不使用函數(shù)重載的情況下,完成這種函數(shù)的編寫(xiě)。同時(shí)可以得到更好的類(lèi)型安全性。
強(qiáng)制類(lèi)型轉(zhuǎn)換是非常重要的特性,特別是在底層開(kāi)發(fā)時(shí)。但也是雙刃劍,往往引來(lái)很隱蔽的錯(cuò)誤。強(qiáng)制類(lèi)型轉(zhuǎn)換很多情況下是無(wú)理的,通常都是軟件的設(shè)計(jì)問(wèn)題造成的。但終究還是有一些情況,需要它來(lái)處理。
設(shè)想這樣一個(gè)場(chǎng)景:兩個(gè)一模一樣的類(lèi)型,但它們分屬不同的函數(shù)。(這種情形盡管不多見(jiàn),但還是存在的。這往往是混亂設(shè)計(jì)的結(jié)果。當(dāng)然也有合理的情況,比如
來(lái)自?xún)蓚€(gè)不同庫(kù)的類(lèi)型)。我現(xiàn)在需要寫(xiě)一個(gè)函數(shù),能夠同時(shí)使用這兩個(gè)類(lèi)型。比較安全一些的,可以用函數(shù)重載。但是兩個(gè)重載的函數(shù)代碼是一樣的,典型的冗余
代碼。當(dāng)然也可以針對(duì)其中一個(gè)結(jié)構(gòu)編寫(xiě)代碼,然后在使用時(shí),對(duì)另一個(gè)結(jié)構(gòu)的實(shí)例執(zhí)行強(qiáng)制類(lèi)型轉(zhuǎn)換。但是,強(qiáng)制類(lèi)型轉(zhuǎn)換畢竟不是件好事。因此,我們也可以構(gòu)
造一個(gè)concept,讓它描述這兩個(gè)類(lèi)型。然后在編寫(xiě)函數(shù)時(shí)使用這個(gè)concept,當(dāng)這兩個(gè)類(lèi)型都與concept綁定后,便可以直接使用這兩個(gè)類(lèi)
型,而沒(méi)有類(lèi)型安全和代碼冗余的問(wèn)題。
(順便提一下,這種方式也可以運(yùn)用在類(lèi)型不同的情況下。比如兩個(gè)類(lèi)型不完全相同,但是基本要素都一樣。那么就可以使用
concept_map的適配功能,
將兩個(gè)類(lèi)型統(tǒng)一在一個(gè)concept下。這種方式相比oop的Adapter模式,更加簡(jiǎn)潔。adapter本身是一個(gè)container,它所實(shí)現(xiàn)的接
口函數(shù),都必須一一轉(zhuǎn)發(fā)到內(nèi)部的對(duì)象,編寫(xiě)起來(lái)相當(dāng)繁瑣。但在concept_map中,對(duì)于那些符合concept描述的函數(shù)無(wú)需另行處
理,concept會(huì)自動(dòng)匹配,只需對(duì)那些不符合要求的函數(shù)執(zhí)行適配。)
前面說(shuō)過(guò),指針數(shù)組的等價(jià)性體現(xiàn)了一種直觀的編程模型。但是,指針和數(shù)組畢竟還是存在很多差別,比如指針僅僅表達(dá)了一組對(duì)象在內(nèi)存中的位置,但并未攜帶對(duì)象數(shù)量的信息。因而,當(dāng)數(shù)組退化成指針時(shí),便已經(jīng)失去了數(shù)組的身份:
void func(int* x);
int a[20];
func(a);
這里,在函數(shù)func中已經(jīng)無(wú)法將a作為數(shù)組處理,因?yàn)闊o(wú)法知道變成int*后的a有多大來(lái)避免越界。甚至我們無(wú)法把a(bǔ)作為多個(gè)對(duì)象構(gòu)成的內(nèi)存塊看待,因?yàn)槲覀儾恢来笮 R虼耍挥酗@式地給出數(shù)組大小,才能使用:
void func(int* x, long size);
但是,在concept的作用下,數(shù)組和指針得以依然保持它們的等價(jià)性的情況下,解決數(shù)組退化問(wèn)題。考慮這樣兩個(gè)函數(shù):
void func1(Pointer x);
void func2(Container x);
其中,Pointer是代表指針的concept,而Container則是代表容器的concept。必須注意的是,Pointer是嚴(yán)格意義上的指
針,也就是說(shuō)無(wú)法在Pointer上執(zhí)行迭代操作。Pointer只能作為指針使用,只具備dereference的能力(很像java的“指針”,不是
嗎?concept在沒(méi)有放棄C的底層特性的情況下也做到了。)。而Container則是專(zhuān)門(mén)用來(lái)表達(dá)容器的concept,其基本的特性便是迭代。在
func1中,無(wú)法對(duì)形參x執(zhí)行迭代,僅僅將其作為指向一個(gè)對(duì)象的指針處理,保證其安全性。而對(duì)于需要進(jìn)行迭代操作的func2而言,x則是可以遍歷的。
于是,對(duì)于同一個(gè)數(shù)組a,兩個(gè)函數(shù)分別從不同的角度對(duì)其進(jìn)行處理:
int a[20];
func1(a); //a直接作為指針處理,但不能迭代
func2(a); //a作為容器處理,可以迭代,并且其尺寸信息也一同傳入
此處實(shí)際上是利用了concept對(duì)類(lèi)型特性的描述作用,將具有兩重性的數(shù)組類(lèi)型(數(shù)組a即代表了數(shù)組這個(gè)容器,也代表了數(shù)組的起始地址)以不同特征加以
表達(dá),以滿(mǎn)足不同應(yīng)用的需求。數(shù)組仍然可以退化成指針,C的直觀模型得到保留,在很多特殊的場(chǎng)合發(fā)揮作用。但在其他應(yīng)用場(chǎng)景,可以更加安全地使用數(shù)組。
總結(jié)
綜上所述,C++未能真正延續(xù)C的直觀簡(jiǎn)潔,主要是由于動(dòng)多態(tài)的一些基礎(chǔ)設(shè)施破壞了C的編程模型。因而,我們可以通過(guò)放棄動(dòng)多態(tài),及其相關(guān)的一些技術(shù),代
之以更加“和諧”的runtime
concept,使得C++在基本保留C的編程模型的同時(shí),獲得了相比原來(lái)更好的軟件工程特性。至此,這種改變后的C++(如果還能稱(chēng)為C++的話(huà))擁有
如下的主干特性:
1、SP,來(lái)自于C。
2、完全pod化。
3、OB。保留了封裝和RAII。盡管也保留了繼承,但其作用僅限于代碼復(fù)用,禁止基于繼承的隱式類(lèi)型轉(zhuǎn)換。
4、GP,包括static和runtime concept。這是抽象高級(jí)特性的核心和基石。
這樣的語(yǔ)言特性實(shí)質(zhì)上比現(xiàn)有的C++更加簡(jiǎn)潔,但是其能力更加強(qiáng)大。也比C++更易于貼近C的編程模型,以便適應(yīng)底層的開(kāi)發(fā)。我不能說(shuō)這樣的變化是否會(huì)產(chǎn)生一個(gè)更好的語(yǔ)言,但是我相信這些特性有助于構(gòu)造更加均衡統(tǒng)一的語(yǔ)言。