什麼是C++/CLI? C++/CLI描繪的是一種多元組合,此處的 C++ 當然是指 Bjarne Stroustrup 在 Bell 實驗室發明的C++編程語言。它支持速度和執行 文件的大小都得到優化的靜態對象模型。但除了堆內存分配以外,它不支持運行時程序對對象的更改。它答應對底層機器進行無限制的訪問,但對於正在運行的程序中的活動類型、以及相關的程序基礎構造,它的訪問能力卻非常有限、或者根本就不可能。我在微軟的同事 Herb Sutter,也是C++/CLI的主架構師,認為C++是一個混凝土語言。
“CLI”即公共語言基礎結構(Common Language InfrastrUCture),這是一個支持動態組件編程模型的多層架構。在許多方面,它所表示的對象模型和C++的完全相反。它是一個運行時軟件層 ,一個虛擬執行系統,運行在應用程序和底層操作系統之間。對底層機器的訪問受到相當嚴格的限制。支持對運行中程序的活動類型以及關聯程序的基礎構造 進行存取——發現和建構。斜線“/”表示 C++ 和 CLI 之間的一種綁定(binding),有關這種綁定的細節構成本專欄的常規主題。
所以,對於“什麼是C++/CLI?”這個問題第一個最近似的答案是:它是靜態C++對象模型到動態CLI組件對象模型的一種綁定, 簡言之,它就是你如何用C++進行.NET編程,而不是用C#或Visual Basic.NET。象C#和CLI自己一樣,C++/CLI正在經歷 ECMA(歐洲計算機制造商協會) 標准化並最終要經歷ISO標准認證。
公共語言運行時(CLR)是微軟版的CLI,專門用於 Windows 操作系統,同樣,Visual C++ 2005是C++/CLI 的實現。
第二個近似答案是:我覺得C++/CLI在C++內集成.NET編程模型與以前貝爾實驗室在當時的C++中用模板集成泛型編程一樣有異曲同工之處。兩種情況中,你在現有C++代碼庫上的投資以及你現有的C++專業技術都得到保護。這是C++/CLI設計的一個基本 要求。
學習C++/CLI
一種C++/CLI語言的設計有三個層面,這三個層面也適用於所有語言:語言層語法到公共類型系統(CTS) 的映射;選擇為程序員直接操作而公開的底層CLI基本組織結構 的具體程度;以及選擇要提供的超越CLI直接支持的附加功能。
第一個層面是所有CLI語言在很大程度上都共有的,第二個層面和第三層面是某一CLI語言區別於其它語言的地方。根據所要解決的問題,你可以選擇某一 種語言,也可以將多種CLI語言結合起來。學習C++/CLI語言需要把握這三個設計層面。
怎樣將C++/CLI 映射到CTS?
了解底層CTS 對學習C++/CLI非常有幫助,它主要包括三個常規類類型:
多態引用類型,其用於所有的類繼續;
非多態值類型,其用於實現需要運行時效率的具體類型,如數字類型;
抽象接口類型,其用於定義一個實現該接口的一組引用類型或值類型共同使用的公共操作集;
在設計方面,雖然CTS到一組內置的語言類型的映射對於所有CLI語言來說都是共同的,當然,每一種CLI語言的語法各不相同。例如,在C#中,我們可以 這樣來定義一個抽象基類 Shape,從這個類派生特定的幾何模型對象。
abstract class Shape { ... } // C#
而在C++/CLI中,可以象下面這樣寫,以表示完全相同的底層引用類型:
ref class Shape abstract { ... }; // C++/CLI
在底層 IL(中間語言)中,以上兩種聲明以完全相同的方式表示。同樣,在C#中,我們可以用下面的代碼來定義一個具體的 Point2D 類 :
struct Point2D { ... } // C#
而在C++/CLI中寫成:
value class Point2D { ... }; // C++/CLI
借助 C++/CLI 支持的類類型家族表現了一種本機方式的 CTS 集成。它確定了你的語法選擇,例如:
class native {};
value class V {};
ref class R {};
interface class I {};
CTS 也支持枚舉類類型,其行為方式與本機枚舉稍微有些區別,C++/CLI對二者都提供支持:
enum native { fail, pass };
enum class CLIEnum : char { fail, pass};
同樣,CTS支持其自己的數組類型,其行為也與本機數組類型有一定差別,微軟同樣對二者提供支持:
int native[] = { 1,1,2,3,5,8 };
array<int>^ managed = { 1,1,2,3,5,8 };
那種認為任何一種 CLI 語言比另一種語言更接近或幾乎就是到底層CLI的映射是不精確的。相反,每一種CLI語言都只是表達了自己對底層CLI對象模型的一種 見解。在下一節你將更清楚地看到這一點。
在設計CLI語言時必須考慮的第二個設計層面是要將什麼程度的底層CLI實現模型結合到該語言中。這個語言解決什麼樣的問題?要解決這些問題必須要什麼樣的工具? 此外,該語言很可能吸引哪一類程序員?
下面,我們利用發生在托管堆中的值類型問題。在許多情況下,值類型可以在托管堆中找到自己:
通過隱式的框入/框出操作(boxing)——當值類型的某個實例被賦值給一對象時,或者通過某個未被改寫的值類型調用一個虛擬方法時;
當值類型被當作為引用類類型的成員時;
當值類型被當作CLI數組元素存儲時;
是否答應程序員處理這種值類型地址是設計CLI語言時必須要解決的問題。
存在的問題?
位於托管堆中的任何對象在垃圾回收器進行清掃收縮的過程中都有可能遭遇重新分配,指向這些對象的任何指針必須被追蹤並在運行時得到更新,而程序員 無法自己手動追蹤它們,因此,假如你被答應用某個可能在托管堆中的值類型的地址,那麼除了本機指針外,還需要一個追蹤形態的指針。
到底該怎樣去權衡呢?一方面,需要考慮簡潔和安全。直接引入對一個或一組追蹤指針的支持會使語言變得更復雜。假如不提供這種支持,由於所需的復雜程度降低,從而可以找到的程序員人群就會增加。此外,答應程序員訪問這些生命期短暫的值類型,則增加了程序員出錯的可能性。她經意或不經意地對內存做一些危險動作。不支持追蹤指針,可以潛在地創建較安全的運行時環境。
另一方面,必須考慮效率和靈活性。每次將值類型賦值給相同的對象,該值都會發生新的框入/框出操作。答應訪問這種經過框入/框出操作的值類型 ,就答應在內存中進行更新操作,這樣便可能提供重要的性能改進。沒有某種形式的追蹤指針,你將無法用指針算法遍歷CLI數組,這意味著CLI數組將不能 融入STL(標准模板庫)中的迭代器模式,也無法與泛型算法協同工作。答應訪問框入/框出值類型將會大大提高設計的靈活性。
在C++/CLI 中,微軟選擇提供一系列在托管堆中處理值類型的尋址模式:
int ival = 1024;
int^ boxedi = ival;
array<int>^ ia = gcnew array<int>{1,1,2,3,5,8};
interior_ptr<int> begin = &ia[0];
value struct smallInt { int m_ival; ... } si;
pin_ptr<int> ppi = &si.m_ival;
典型的 C++/CLI 開發人員是一個經驗豐富的系統程序員,其任務是提供底層架構以及作為基礎的核心應用,以此為基礎來構建未來。她必須解決可伸縮性和性能相關的問題,並且必須從系統一級來看待底層 CLI。某種 CLI 語言的細節標准反映了其程序員的面貌。
復雜性本身並不是對質量的否定,人類生命比單核細胞復雜得多,這當然不是一件壞事,然而,當單一概念的表達變得復雜化以後,這經常被認為是一件壞事。在C++/CLI中,CLI開發團隊已經 嘗試提供一種優雅的方式來表達一個復雜的主體。
附加功能
第三個設計層面是特定語言層功能要超過被CLI直接支持的功能,這樣就需要建立一種語言層支持與CLI底層實現模型之間的映射。 在某些情況下,這是做不到的,因為該語言無法調解CLI的行為,在基類的構造函數和析構函數中解決虛函數便是例子。為了在這種情況中反映ISO-C++語義,需要在每個基類的構造函數和析構函數中 重新安排虛表。這是不可能的,因為虛表操作是由運行時托管的,而非單獨的語言托管。 因此,這一設計層面是優越性和可行性的折中。C++/CLI 提供的附加功能主要有三個方面:
引用類型的資源獲取(Resource Acquisition)形式是Initialization(RAII), 尤其是為被稱作占據稀有資源的垃圾回收類型確定性終止化(deterministic finalization)提供一個自動化的機制;
與C++拷貝構造函數和拷貝賦值操作符相關的深度拷貝語義形式,但它不能擴展到值類型;
除了 CLI泛型機制之外——這原來是我第一個專欄的主題,還為CTS類型提供C++模板的直接支持,另外,還提供用於 CLI 類型的 STL 可驗證版本;
讓我們看一個簡單的例子:確定性終止化問題。與對象關聯的內存被垃圾回收器回收之前,若存在與之相關連的 Finalize 方法,該方法將會被調用。你可以把 該方法看作是一種超級析構函數,因為它不依靠於該對象程序的生命期,它被稱為終止化。調用 Finalize 方法的時間,甚至是否調用它是未定義的。這就是垃圾回收器不確定的終止化操作含義之所在。
不確定性終止化在進行動態內存治理時可以很有效地工作,當可用內存空間嚴重不足時,垃圾回收器會發揮作用並解決問題。但是當對象涉及的是某些重要資源,比如數據庫連接、某種 類型的鎖、本地堆內存時,不確定性終止化的表現卻不盡人意。在這種情況下,最好是盡快釋放不再需要的資源。目前CLI采用的解決辦法是:某個類在其 IDisposable 接口的 Dispose 方法中釋放資源,這裡的問題是 Dispose 需要顯式調用,因此它不可能被執行。
C++的基本設計模式是前述的資源獲取(Resource Acquisition )即初始化(Initialization),它意味著類通過構造函數 獲取資源,相反,通過析構函數來釋放資源。在類對象的生存期內是自動治理的。
以下是引用類型釋放資源的過程:
用析構函數壓縮在釋放資源過程中必須的代碼;
自動調用綁定到類對象生存期的析構函數;
CLI中,引用類型的類沒有類析構的概念,因此,析構函數被映射到底層實現中另外的東西上,編譯器則在內部完成如下轉換:
類具備其基類列表,從接口 IDisposable 延伸繼續;
析構函數被轉換成IDisposable 的 Dispose 方法;
這僅僅完成了一半,還需要一種析構函數的自動調用途徑。支持引用類型專用的基於堆棧的符號,也就是說其生命期與其聲明的范圍相關聯。編譯器 在內部轉換符號,在托管堆中分配引用對象。隨著范圍的終止,編譯器插入一個對 Dispose 方法的調用——用戶定義的析構函數。與該對象關聯的實際內存的回收仍然在垃圾回收器的掌控之下。例如如 Figure 1 所示。
C++/CLI 不僅僅是C++到治理世界的擴展,相反,它表現了一種完全的編程范例,類似於早期多重繼續和泛型編程范例集成到該語言一樣,我認為這個團隊完成了一項傑出的工作。
那麼,你是如何看待 C++/CLI 的呢?
C++/CLI代表了本地和托管編程的綜合,在這個反復過程中,這種綜合通過即獨立而又等同的源碼級和二進制元素共同體來完成,包括混合模式(本機和CTS類型的源碼級混合,以及本機和CIL對象文件的二進制混合), 本地類型和CTS類型的混合,新增了混合本地對象和CIL對象的二進制文件),純模式(本機和CTS類型的源碼級混合,所有編譯過的 CIL 對象文件),本機類(僅通過專門的包裝類才可以操控 CTS 類型),以及 CTS 類(只能以指針形式操控本機類型)。
當然,C++/CLI 程序員也可以選擇單獨用 CLI 類型來編程,在這種方式中提供能被寄宿的可驗證代碼,比如 SQL Server 2005 中的存儲過程。
現在,回到什麼是 C++/CLI 的問題,它是進入.NET編程模型的第一道門檻,有了 C++/CLI,你不僅具備了遷移 C++ 源代碼庫的途徑,同時還可以遷移 C++ 專業技術。這讓我感覺非常舒服。