從C++到C++/CLI(1)

就像我們作出其它任何選擇一樣,在選擇之前最重要的是先要清楚爲什麼作出這樣或那樣的選擇——C++/CLI到底提供了哪些優勢?爲什麼我們(標準C++程序員)要選擇C++/CLI而不是C#?我們能夠得到什麼?CLI平臺會不會束縛C++的能力?

 

這些都是來自標準C++社區的疑問。從google上面的討論看來,更多來自標準C++社區的程序員擔心的是C++/CLI會不會約束標準C++的能力,或者改變標準C++發展的方向,也有一部分人對C++/CLI的能力持懷疑態度。另外一些人則是詢問C++/CLI能夠帶來什麼。

 

這些被提出的問題在google上面一一得到了答案。好消息是:情況比樂觀的人所想象的或許還要更好一些——

 

 

世界改變了嗎?

 

對於諳於標準C++的程序員來說,最爲關心的還是:在C++/CLI中,世界還是他們熟悉的那個世界嗎?在標準C++的世界裏,他們手裏的各種魔棒——操作符重載|模板|多繼承(語言),STL|Boost|ACE(庫)——還能揮舞出五彩繽紛的火焰嗎?是不是標準C++到了.NET環境下就像被拔掉了牙的老虎一樣——Managed C++ Extension的陰影是不是還籠罩在他們的心頭?

 

答案是:以前你所能做的,現在仍然能做,世界只是變得更廣闊了——

 

 

什麼是C++/CLI

 

l         C++/CLI是一集標準化的語言擴展(對標準C++進行擴展),而並非另起爐竈的另一門新語言。所以C++/CLI是標準C++的一個超集。

 

l         C++/CLI是一門ECMA標準[1](並且會被提交給ISO標準化委員會),而不是微軟的專有語言。參與C++/CLI標準的修訂的有很多組織(或公司),其中包括Edison Design GroupDinkumware公司等,微軟在把C++/CLI標準草案提交給ECMA組織後就放棄了對其的控制權,而是將它作爲一份公開發展的標準,任何使用C++/CLI的用戶都可以爲它的發展提出自己的建議。

 

l         C++/CLI的目的是把C++帶到CLI平臺上,使C++能夠在CLI平臺上發揮最大的能力。而並非把C++約束在CLI平臺(CLI本身也是ISO標準化的)上。相反,原來標準C++的能力絲毫沒有減弱,並且,通過C++/CLI中的標準擴展,C++具有了原來沒有的動態編程能力以及一系列的first class.NET特性。這些擴展並非是專有的,而是以一種標準的方式呈現。

 

 

C++/CLI有什麼優越性?

 

l         動態編程和refelection——標準C++是一門非常靜態的語言,其原則是儘量在編譯期對程序的合法性和邏輯作出檢查。而在運行時的動態信息方面,標準C++是有所欠缺的。例如,標準C++在運行期能夠對動態對象進行查詢的就只有typeid操作符,而typeid()返回的typeinfo類雖然是個唯一標識,但是也僅僅止於“唯一”而已,首先標準C++並未規定typeinfo的底層二進制表示,所以用它作爲跨平臺的類唯一標識符就不可能了,其次typeinfo類幾乎僅僅就表示類名字而已,這種非常“薄”的運行時類型信息阻止了標準C++在分佈式領域的能力(IDL就是爲了彌補標準C++在運行期類型信息的不足,但是IDL對於擁有元數據的語言如JAVAC#根本就不是必須的,同時IDL也使C++在分佈式領域的使用不那麼簡易)。由於標準C++Native特點,所以其代碼一經編譯便幾乎喪失所有的類型信息,從而使得運行期無法對程序本身做幾乎任何的改動,換句話說,標準C++的代碼一經編譯就幾乎變成了死的。而C++/CLI改變了這一現狀,C++/CLI擁有完備的元數據,允許程序在運行期查詢完整的類型信息,並可以由類型信息動態創建對象,甚至可以動態創建類型,添加方法等等,這種強大的運行期的動態特性對於現代應用領域(例如分佈式WEB應用)是必須的。

 

l         GC——現在誰也不會說“往C++中加入GC就是終結了C++”這種話了。就連Bjarne Stroustrup也同意如果C++要被用於大型或超大型的軟件開發中去,最好要有一個良好的可選的GC支持。GC與否,已經不再是個值得爭論的問題,問題是,我們如何把它實現的更好——C++/CLI中的GC是目前最爲強大的GC機制之一——分代垃圾收集。GC的好處是可以簡化軟件的開發模型,在效率並非極其關鍵的領域,GC可以很大程度上提高生產率。C++/CLI中的GC很重要的一點是:它正是可選的。這一點不同於JAVAC#,對於後者,GC無處不在,對象只能分配在託管堆上。而在C++/CLI中,如果把你的對象分配在Native Heap上,你就得到和標準C++一樣的高效內存管理。如果把對象分配在Managed Heap上,那麼該對象的內存就由GC來自動回收。這種混合式的內存管理環境和C++/CLI的定位有關——畢竟,C++/CLI的定位是.NET平臺上的系統級編程語言,所以效率以及對底層的控制很重要,故保留了Native Heap。後面你會看到這種編程環境的優點。

 

l         BCL——.NET平臺上的基礎類庫,BCL中豐富的類極大的方便了開發者。C++/CLI可以完全使用BCL中的任何類。

 

l         可移植性——毫無疑問,可移植性是個至關重要的問題,特別是對於標準C++社羣的人們。C++/CLI對這個問題的答案是:“如果你的代碼不依賴於本地二進制庫,就可以“一次編譯,隨處運行(在.NET平臺上)”(使用“/clr:pure”編譯選項將代碼編譯成可移植的純MSIL代碼)。如果你的代碼某部分依賴於本地的二進制庫(C輸入輸出流庫),那麼這些部分仍然是源代碼可移植的,而其它部分則可以“一次編譯,隨處運行”。對於標準C++來說,向來保證的只是源代碼的可移植性,所以我們並沒有失去什麼,相反,如果遵守協定——不用本地二進制庫,例如,用BCL裏的輸入輸出流庫代替C輸入輸出流庫——你就可以得到“一次編譯,隨處運行”的承諾,也就是說,你的代碼經過編譯(/clr:pure)後可以在其它任何.NET平臺上運行——UnixLinux下的Mono(移植到UnixLinux下的.NET),以及FreeBSDMac OSX下的Rotor.NET的開放源代碼項目),等等。

 

習慣了標準C++輸入輸出流的程序員可能要抱怨了——我們爲什麼要使用BCL裏面的輸出輸出流?標準的iostream已經很好了!這裏其實有一個解決方案,使用 iostream的代碼之所以不能“一次編譯,隨處運行”是因爲代碼要依賴於本地的二進制lib文件,而如果可以把iostream的實現重新也編譯成純MSIL代碼,那麼使用它的代碼編譯後就完全可隨處運行了。目前,這是個有待商榷的方案。不過,至少當面對“總得依賴於某些平臺相關的二進制代碼”這種情況時,可以把平臺相關的代碼封裝成DLL文件——對各個目標平臺編譯成不同的二進制版本,而程序的其它部分仍然只需一次編譯即可,只要使用.NETP/Invoke就可以對不同平臺調用相應的DLL了。

 

l         效率——作爲.NET平臺上的系統級編程語言,C++/CLI混合了NativeManaged兩種環境。而不象C#那樣只能進行託管編程。所以相對來說,C++/CLI可以具有更高的效率——前提是你願意把效率敏感的代碼用極具效率的Native C++來寫(當然,誰不願意呢?)另外,因爲標準C++是靜態語言,所以作爲標準C++的一個超集的C++/CLI能夠在編譯期得到更多的優化(靜態語言總是能夠得到更多的優化,因爲編譯器能夠知道更多的信息),從而具有更高的效率。相比之下,C#的編譯期優化就弱了很多。

 

l         混合式的編程環境——這是C++/CLI獨有的一種編程環境。你既可以進行高效的底層開發——對應於C++/CLI的標準C++子集,也可以在效率要求不那麼嚴格的地方使用託管式編程以提高生產率。然後把兩者平滑的聯結在一起,而這一切都在你熟悉的編程語言中完成,你使用你熟悉的編程習慣,熟悉的庫,熟悉的語言特性和風格不需要從頭學習一門新的語言,不需要面對語言之間如何交互的問題。

 

l         習慣——誰也不能小覷習慣的力量。對於標準C++程序員,如果要在.NET平臺上開發,C++/CLI是毫無疑問的首選語言,因爲他們在標準C++中積累起來的任何編程技巧,慣用法,以及對庫的使用經驗,代碼的表達方式等等全都可以“移植”到C++/CLI中。C++/CLI保持對標準C++代碼的完全兼容,同時以最小最一致的語法擴展提供託管環境下編程的必要語義。

 

 

你需要改變什麼?

 

      簡單的答案是,幾乎沒有什麼需要改變的。是的,你看到“幾乎”兩個字,總有些不安心:o)事實是:把現存的C++代碼移植到C++/CLI環境下不用作任何的改變——我曾經用Native C++寫了一個程序,其中用到了STLBoost裏面的LambdaMPLSignal等庫,然後我把編譯選項“/clr”(甚至“/clr:pure”)打開,結果是程序完全通過了編譯。而對於使用C++/CLI進行開發的程序員,則需要熟悉的就是.NET平臺上的編程範式以及庫的使用等,至於以前你所熟悉的標準C++編程的各種編程手法,技巧,各種庫的使用——Just Keep Them!

 

      所以,確切的說,你需要的是學習,而不是改變。

 

 

C++/CLI——優秀的混血兒

 

      C++/CLI最大的成功在於引入了混合式編程的環境,這是一種非常自由的環境,其中NativeManaged代碼可以共存,可以相互溝通,從而完全接納了標準C++的世界,同時也爲另一個世界敞開了大門...

 

    下面就是C++/CLI擴展的幾大關鍵特性——

 

Handlegcnew——通往Managed世界的鑰匙

 

       還記得在Managed C++ Extension世界裏是如何訪問託管類的嗎?醜陋的__gc關鍵字無處不在——事實上,不僅是“醜陋”而已(MC++爲什麼會消亡?)。而在C++/CLI裏則引入了一個新的語法元素,名爲Handle,寫作“^”——你可以把它看成Managed世界裏的Pointer(不過不能進行指針算術)。

 

Handle用於持有Managed Heap上的對象,那麼如何在Managed Heap上創建對象呢?原來的new顯然不能用,那樣會混淆其語義,所以C++/CLI引入了一個對應的gcnew關鍵字,這兩個新的語法元素是操縱Managed世界的關鍵。現在,使用Handlegcnew,你就可以和任何託管類進行溝通。另外,既然有了Handle這個Managed指針,當然,基於另外一些重要原因,Managed世界裏也要有一個和Native引用類似的語法元素——這就是Managed引用“%”——“^”對應“*”,“%”對應“&”,這樣一來,從語法的層面上,指針、引用、以及在堆上創建對象的語法就在兩個世界裏面對稱一致了——哦,等等,還有解引用:對Native Pointer解引用是以“*”,出於模板對形式統一性的要求,對Handle解引用也是用“*”。例如:

SomeManagedClass^ handle = gcnew SomeManagedClass( ... );
handle
->someMethod();
SomeManagedClass
% ref = *handle;

    那麼,既然有gcnew,有沒有gcdelete呢?答案是沒有——雖然它們看起來很對稱。理由是對於託管類,根本就不用回收內存。但更爲重要的還是,delete的語義不僅僅是回收內存,從廣義上說,delete是回收資源的意思,從這個意義上,delete託管類還是Native類的對象都是一個意思。所以,即使你需要delete你的託管類對象,以強制其釋放資源,你也應該用delete,這時候託管類的析構函數會被調用——是的,託管類也有析構函數,它的語義和Dispose()一樣,但是在C++/CLI裏面,你不應該爲你的託管類定義Dispose()函數,而總是應該用析構函數來代替它(編譯器會根據析構函數自動生成Dispose()函數),因爲析構函數有一個最大的優點——

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章