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