close
C++的發展史上,也有著如同漢尼拔翻越阿爾俾斯山遠征。一切還得從C with Class時代說起。
Bjarne曾經反復強調,他創建C++為的是將Simular的抽象能力同C的性能結合起來。於是,在C語言的基礎上,誕生了一種擁有類、繼承、重載等等面向對象機制的語言。在這個階段,C++提供了兩個方面的抽象能力。一種是數據抽象,也就是將數據所要表達的含義通過類型以及依附於類型上的成員表述。另一種則是多態,一種最原始的多態(重載)。
數據抽象,通過被稱為“抽象數據類型(ADT)”的技術實現。ADT的一種方案,就是類。類所提供的封裝性將一個數據實體的外在特征,或者說語義的表述形式,同具體的實現,比如數據存儲形式,分離。這樣所增加的中間層將數據的使用者同數據的實現者隔離,使得他們使用共同的約定語義工作,不再相互了解彼此的細節,從而使得兩者得以解耦。
多態則是更加基礎更加重要的一種特性。多態使得我們得以用同一種符號實現某種確定的語義。多態的精髓在於:以一種形式表達一種語義。在此之前,我們往往被迫使用不同的符號來代表同一種抽象語義,為的是適應強類型系統所施加的約束。比如:
//代碼#1
int add_int(int lhs, int rhs);
float add_float(float lhs, float rhs);
很顯然,這兩個函數表達的語義分別是“把兩個int類型值加在一起”和“把兩個float類型值加在一起”。這兩個語義抽象起來都表達了一個意思:加。
我們在做算術題的時候是不會管被計算的數字是整數還是實數。同樣,如果能夠在編程的時候,不考慮算術操作對象的類型,只需關心誰和誰進行什麽操作,那麽會方便得多。當C++引入重載後,這種願望便得以實現:
//代碼#2
int add(int lhs, int rhs);
float add(float lhs, float rhs);
重載使得我們只需關心“加”這個語義,至於什麽類型和什麽類型相加,則由編譯器根據操作數的類型自動解析。
從某種意義上說,重載是被長期忽視,但卻極為重要的一個語言特性。在多數介紹OOP的書籍中,重載往往被作為OOP的附屬品,放在一些不起眼的地方。它的多態本質也被動多態的人造光環所設遮蔽。然而,重載的重要作用卻在實踐中潛移默化地體現出來。重載差不多可以看作語言邁入現代抽象體系的第一步。它的實際效用甚至要超過被廣為關註的OOP,而不會像OOP那樣在獲得抽象的同時,伴隨著不小的副作用。
隨著虛函數的引入,C++開始具備了頗具爭議的動多態技術。虛函數是一種依附於類(OOP的類型基礎)的多態技術。其技術基礎是後期綁定(late-binding)。當一個類D繼承自類B時,它有兩種方法覆蓋(override)B上的某個函數:
//代碼#3
class B
{
public:
void fun1();
virtual void fun2();
};
class D:public B
{
public:
void fun1();
void fun2();
};
當繼承類D中聲明了同基類B中成員函數相同函數名、相同簽名的成員函數,那麽基類的成員函數將被覆蓋。對於基類的非虛成員函數,繼承類會直接將其遮蔽。對於類型D的使用者,fun1代表了D中所賦予的語義。而類型D的實例,可以隱式地轉換成類型B的引用b,此時調用b的fun1,則執行的是類B的fun1 定義,而非類D的fun1,盡管此時b實際指向一個D的實例。
但是,如果繼承類覆蓋了基類的虛函數,那麽將得到相反的結果:當調用引用b的fun2,實際上卻是調用了D的fun2定義。這表明,覆蓋一個虛函數,將會在繼承類和基類之間的所有層次上執行覆蓋。這種徹底的、全方位的覆蓋行為,使得我們可以在繼承類上修飾或擴展基類的功能或行為。這便是OOP擴展機制的基礎。而這種技術被稱為動多態,意思是基類引用所表達的語義並非取決於基類本身,而是來源於它所指向的實際對象,因此它是“多態”的。因為一個引用所指向的對象可以在運行時變換,所以它是“動”的。
隨著動多態而來的一個“副產品”,卻事實上成為了OOP的核心和支柱。虛函數的“動多態”特性將我們引向一個極端的情況:一個都是虛函數的類。更重要的,這個類上的虛函數都沒有實現,每個虛函數都未曾指向一個實實在在的函數體。當然,這樣的類是無法直接使用的。有趣的是,這種被稱為“抽象基類”的類,迫使我們繼承它,並“替它”實現那些沒有實現的虛函數。這樣,對於一個抽象基類的引用,多態地擁有了繼承類的行為。而反過來,抽象基類實際上起到了強迫繼承類實現某些特定功能的作用。因此,抽象基類扮演了接口的角色。接口具有兩重作用:一、約束繼承類(實現者)迫使其實現預定的成員函數(功能和行為);二、描述了繼承類必定擁有的成員函數(功能和行為)。這兩種作用促使接口成為了OOP設計體系的支柱。
C++在這方面的進步,使其成為一個真正意義上具備現代抽象能力的語言。然而,這種進步並非“翻越阿爾俾斯山”。充其量也只能算作“翻越比利牛斯山”。對於C++而言,真正艱苦的遠征才剛開始,那令人生畏的“阿爾俾斯山”仍在遙遠的前方。
同漢尼拔一樣,當C++一腳邁入“現代抽象語言俱樂部”後,便面臨兩種選擇。或者在原有基礎上修修補補,成為一種OOP語言;或者繼續前進,翻越那座險峻的山峰。C++的漢尼拔——Bjarne Stroustrup——選擇了後者。
從D&E的描述中我們可以看到,在C++的原始設計中就已經考慮“類型參數”的問題。但直到90年代初,才真正意義上地實現了模板。然而,模板只是第一步。諸如Ada等語言中都有類似的機制(泛型,generic),但並未對當時的編程技術產生根本性的影響。
關鍵性的成果來源於Alex Stepanov的貢獻。Stepanov在後來被稱為stl的算法-容器庫上所做的努力,使得一種新興的編程技術——泛型編程(Generic Programming,GP)——進入了人們的視野。stl的產生對C++的模板機制產生了極其重要的影響,促使了模板特化的誕生。模板特化表面上是模板的輔助特性,但是實際上它卻是比“類型參數”更加本質的機能。
假設我們有一組函數執行比較兩個對象大小的操作:
//代碼#4
int compare(int lhs, int rhs);
int compare(float lhs, float rhs);
int compare(string lhs, string rhs);
重載使得我們可以僅用compare一個函數名執行不同類型的比較操作。但是這些函數具有一樣的實現代碼。模板的引入,使得我們可以消除這種重復代碼:
//代碼#5
template int compare(T lhs, T rhs) {
if(lhs==rhs)
return 0;
if(lhs>rhs)
return 1;
if(lhs             return -1;
}
這樣一個模板可以應用於任何類型,不但用一個符號表達了一個語義,而且用一個實現代替了諸多重復代碼。這便是GP的基本作用。
接下來的變化,可以算作真正意義上的“登山”了。
如果有兩個指針,分別指向兩個相同類型的對象。此時如果我們采用上述compare函數模板,那麽將無法得到所需的結果。因為此時比較的是兩個指針的值,而不是所指向的對象本身。為了應付這種特殊情況,我們需要對compare做“特別處理”:
//代碼#6
template int compare(T* lhs, T* rhs) {
if(*lhs==*rhs)
return 0;
if(*lhs>*rhs)
return 1;
if(*lhs <*rhs)
return -1;
}
這個“特殊版本”的compare,對於任何類型的指針作出響應。如果調用時的實參是一個指針,那麽這個“指針版”的compare將會得到優先匹配。如果我們將compare改成下面的實現,那麽就會出現非常有趣的行為:
//代碼#7
template
struct comp_impl
{
int operator()(T lhs, T rhs) {
if(lhs==rhs)
return 0;
if(lhs>rhs)
return 1;
if(lhs             return -1;
}
};
template
struct comp_impl
{
int operator()(T* lhs, T* rhs) {
comp_impl ()(*lhs, *rhs);
}
};
template int compare(T* lhs, T* rhs) {
comp_impl ()(*lhs, *rhs);
}
當我們將指針的指針作為實參,調用compare時,神奇的事情發生了:
//代碼#8
double **x, **y;
compare(x, y);
compare居然成功地剝離了兩個指針,並且正確地比較了兩個對象的值。這個戲法充分利用了類模板的局部特化和特化解析規則。根據規則,越是特化的模板,越是優先匹配。T*版的comp_impl比T版的更加“特化”,會得到優先匹配。那麽當一個指針的指針實例化comp_impl,則會匹配T*版的 comp_impl,因為指針的指針,也是指針。T*版通過局部特化機制,剝離掉一級指針,然後用所得的類型實例化comp_impl。指針的指針剝離掉一級指針,那麽還是一個指針,又會匹配T*版。T*版又會剝離掉一級指針,剩下的就是真正可以比較的類型——double。此時,double已無法與 T*版本匹配,只能匹配基礎模板,執行真正的比較操作。
這種奇妙的手法是蘊含在模板特化中一些更加本質的機制的結果。這種意外獲得的“模板衍生產品”可以算作一種編譯時計算的能力,後來被一些“好事者”發展成獨立的“模板元編程”(Template Meta Programming,TMP)。
盡管TMP新奇而又奧妙,但終究只是一種輔助技術,用來彌補C++的一些缺陷、做一些擴展,“撿個漏”什麽的。不過它為我們帶來了兩點重要的啟示:一、我們有可能通過語言本身的一些機制,進行元編程;二、元編程在一定程度上可以同通用語言一起使用。這些啟示對編程語言的發展有很好的指導意義。
模板及特化規則是C++ GP的核心所在。這些語言特性的強大能力並非憑空而來。實際上有一只“幕後大手”在冥冥之中操縱著一切。
假設有一個類型系統,包含n個類型:t1,...,tn,那麽這些類型構成了一個集合T={t1,...,tn}。在當我們運用重載技術時,實際上構造了一組類型的tuple到函數實現的映射: ->fj()。編譯器在重載解析的時候,就是按照這組映射尋找匹配的函數版本。當我們編寫了形如代碼#5的模板,那麽就相當於構建了映射: ->f0()。
而代碼#6,以及代碼#7中的T*版模板,實際上是構造了一個 ->fp()的映射。這裏Tp是T的一個子集:Tp={t' |t'=ti*, ti∈T}。換句話說,特化使泛型體系細化了。利用模板特化技術,我們已經能夠(笨拙地)分辨浮點數、整數、內置類型、內置數組、類、枚舉等等類型。具備為類型劃分的能力,也就是構造不同的類型子集的能力。
現在,我們便可以構造一個“泛型體系”:G={T} U T U Tp U Ta U Ti U Tf U Tc ...。其中,Tp是所有指針類型,Ta是數組,Ti是整數,Tf是浮點數,Tc是類等等。但是如果我們按照泛化程度,把G中的元素排列開:{T, Tp, Ta, Ti,...,t1,...,tn}。我們會發現這中間存在一些“斷層”。這些斷層位於T和Tp等之間,以及Tp等與ti等之間等等。這表明在C++98/03中,抽象體系不夠完整,存在缺陷。

問題點數:100 回復次數:185 顯示所有回復顯示星級回復顯示樓主回復 修改 刪除 舉報 引用 回復

加為好友
發送私信
在線聊天
longshanks
longshanks
等級:
發表於:2007-12-17 12:29:551樓 得分:0
所以,到目前為止,C++還沒有真正翻越阿爾俾斯山裏那座最險峻的山峰。這正是C++0x正在努力做的,而且勝利在望。
在C++0x中,大牛們引入了first-class的concept支持。concept目前還沒有正式的法定描述(以及合理的中文翻譯)。通俗地講,concept描述了一個類型的(接口)特征。說具體的,一個concept描述了類型必須具備的公共成員函數,必須具備的施加在該類型上的自由函數(和操作符),以及必須具備的其他特征(通過間接手段)。下面是一個典型的concept:
concept has_equal
{
bool T::equal(T const& v);
};
這個concept告訴我們它所描述的類型必須有一個equal成員,以另一個同類型的對象為參數。當我們將這個concept施加在一個函數模板上,並作為對類型參數的約束,那麽就表明了這個模板對類型參數的要求:
template bool is_equal(T& lhs, T const& rhs) {
return lhs.equal(rhs);
}
如果實參對象的類型沒有equal成員,那麽is_equal將會拒絕編譯通過:這不是我要的!
concept是可以組合的,正式的術語叫做“refine”。我們可以通過refine進一步構造出約束更強的concept:
concept my_concept : has_equal , DefaultConstructable , Swappable {}
refine獲得的concept將會“繼承”那些“基concept”的所有約束。作為更細致的組合手段,concept還可以通過!操作符“去掉”某些內涵的concept約束:
concept my_concept1 : has_equal , !DefaultConstructable {}
這個concept要求類型具備equal成員,但不能有默認構造函數。
通過這些手段,concept可以“無限細分”類型集合T。理論上,我們可以制造一連串只相差一個函數或者只相差一個參數的concept。
一個concept實際上就是構成類型集合T的劃分的約束:Tx={ti | Cx(ti)==true, ti∈T}。其中Cx就是concept所構造的約束。不同的concept有著不同範圍的約束。這樣,理論上我們可以運用concept枚舉出類型集合 T的所有子集。而這些子集則正好填補了上述G中的那些斷層。換句話說,concept細化了類型劃分的粒度,或者說泛型的粒度。使得“離散”的泛型系統變成“連續”的。
當我們運用concept約束一個函數模板的類型參數時,相當於用concept所描述的類型子集構建一個映射: ->fx()。凡是符合tuple  的類型組合,對應fx()。所以,從這個角度而言,函數模板的特化(包括concept)可以看作函數重載的一種擴展。在concept的促進下,我們便可以把函數模板特化和函數重載統一在一個體系下處理,使用共同的規則解析。
在目前階段,C++差不多已經登上了“抽象阿爾俾斯山”的頂峰。但是就如同漢尼拔進入意大利後,還需要面對強盛的羅馬共和國,與之作戰那樣。C++的面前還需要進一步將優勢化作勝利。要做的事還很多,其中最重要的,當屬構建Runtime GP。目前C++的GP是編譯時機制。對於運行時決斷的任務,還需要求助於OOP的動多態。但是C++領域的大牛們已經著手在Runtime GP和Runtime Concept等方面展開努力。這方面的最新成果可以看這裏、這裏和這裏。相信經過若幹年的努力後,GP將會完全的成熟,成為真正主流的編程技術。

坎尼會戰之後,漢尼拔已經擁有了絕對的優勢。羅馬人已經戰敗,他同羅馬城之間已經沒有任何強大的敵對力量,羅馬人也已經聞風喪膽,幾無鬥誌。但是,漢尼拔卻犯下了或許令他一生後悔的錯誤。他放過了羅馬城,轉而攻擊羅馬的南部城邦和同盟。他低估了羅馬人的意誌,以及羅馬同盟的牢固程度。羅馬人很快結束了最初的混亂,任命了新的執政官,采用了堅壁清野、以柔克剛的新戰略。隨著時間的推移,漢尼拔和他的軍隊限於孤立無援的境地,被迫為了生存而作戰。盡管迫降並占領了幾個羅馬城市,但是終究無法再次獲得給予羅馬人致命一擊的機會。
漢尼拔的戰略錯誤實際上在從新迦太基城出發之前已經註定。因為漢尼拔對羅馬人的遠征的根本目的並非擊潰並占領羅馬,而是通過打擊羅馬,削弱他們的勢力,瓦解他們的聯盟。以達到尋求簽訂和平協議的目的。然而這種有限戰略卻使導致了他的最終失敗。

不幸的是,C++或多或少地有著同漢尼拔一樣的戰略錯誤。C++最初的目的基本上僅僅局限於“更好的C”。並且全面兼容C。這在當時似乎很合理,因為C可以算作最成功的“底層高級語言”,擁有很高的性能和靈活性。但是,C的設計並未考慮將來會有一個叫做“C++”的語言來對其進行擴展。結果很多優點對於C而言是優點,但卻成了C++的負擔。比如,C大量使用操作符表達語法結構,對於C而言顯得非常簡潔,但對於C++,則使其被迫大規模復用操作符,為其後出現的很多語法缺陷埋下了伏筆。這一點上,Ada做得相對成熟些。它從Pascal那裏繼承了主要語法,但不考慮兼容。這使得Ada更加完整,易於發展。新語言就是新語言,過分的兼容是鐐銬,不是優勢。而且,合理地繼承語法,同樣可以吸引眾多開發者。從經驗來看,程序員對於語法變化的承受能力還是很強的。他們更多地關心語言的功能和易用性。
另一方面,C++最初並未把目標定在“創建一種高度抽象,又確保性能的語言”。縱觀C++的發展,各種抽象機制並非在完整的規劃或路線圖的指導下加入語言。所有高級特性都是以“添油戰術”零打碎敲地加入語言。從某種程度上來看,C++更像是一種實驗性語言,而非工業語言。C++的強大功能和優點是長期積累獲得的,而它的諸多缺陷也是長期添加特性的結果。
漢尼拔和C++給予我們一個很好的教訓。對於一個試圖在1、20年後依然健康成長的語言,那麽就必須在最初便擁有明確的目標和技術發展規劃。對於以往的語言特性應當擇優而取,不能照單全收。並且在技術上擁有足夠的前瞻性。我們知道,技術前瞻性是很難做到的,畢竟技術發展太快。如果做不到,那就得有足夠的魄力對過去的東西加以取舍。所謂“舍小就大,棄子爭先”。
總體而言,C++在抽象機制的發展方面,還算是成功的。盡管伴隨著不少技術缺陷,但是C++的抽象能力在各種語言中可稱得上出類拔萃。而且C++還在發展,它未來將會發展成什麽形態,不得而知。但是,無論C++是繼續修修補補,還是根本性地變革,它的抽象能力都會不折不扣地保留,並且不斷完善和增強。
arrow
arrow
    文章標籤
    c++ 編程 開發
    全站熱搜

    成功运行 發表在 痞客邦 留言(0) 人氣()