Stan Lippman:C++/CLI全景體驗(轉)

ba發表於2007-08-15
Stan Lippman:C++/CLI全景體驗(轉)[@more@] 最近我訪問了中國的上海和北京,參加在兩地舉辦的微軟 Tech-ED 技術大會,在那裡我非常榮幸地向大家介紹了我們在 C++/CLI 方面的工作。大家的反饋非常之好,特別是中國年輕一代程式設計師對 C++/CLI 的熱愛和理解給我留下了深刻的印象。在那裡,我還認識了來自上海的一位開發人員,同時也是一位技術作者, 李建忠先生。我們經過討論之後決定合作撰寫一系列 C++/CLI 方面的文章,並以“C++/CLI全景體驗”專欄的形式獨家授權於中國《程式設計師》雜誌發表。這篇短文旨在為大家簡單介紹一下我們寫作這個專欄的一些背景 ——有點電影中“定場鏡頭”的味道。

  面對 C++/CLI ,很多人的第一個問題自然是“什麼是 C++/CLI ”,我個人喜歡將其看作是位於靜態程式設計和動態程式設計之間的一座橋樑。 C++/CLI 這個名稱本身就包含著一組術語——而其中最重要的術語卻是最不明顯的那一個。

  首先來看第一個術語“ C++ ”,這當然指的是由 Bjarne Stroustrup 在 Bell 實驗室時發明的 C++ 程式語言。它所支援的是一種為程式碼執行速度和執行體所佔空間所高度最佳化的靜態物件模型。除了堆記憶體分配以外,它不支援在執行時對應用程式進行任何的更改。它允許我們對底層機器進行無限的訪問,但對於正在執行的程式中的活動型別、以及相關的程式基礎構造,它的訪問能力卻非常有限、或者根本就不可能。它是一門非常成功的程式語言,但是它卻不能適應目前的 Web 程式設計環境以及相關的安全問題——這已經成為目前程式設計中一個越來越重要的考量。

  再來看第三個術語“ CLI ”,即通用語言基礎構造( Common Language Infrastructure ),這是一個支援動態元件程式設計模型的多層架構。在許多方面,它所表示的物件模型和 C++ 的完全相反。在 CLI 中,存在一個執行時軟體層(即虛擬執行環境)執行在應用程式和底層作業系統之間,應用程式程式碼對底層機器的訪問會受到相當嚴格的限制;事實上, CLI 根本不允許安全環境中的程式碼進行這樣的訪問。但另一方面, CLI 卻允許我們對正在執行的程式中的活動型別、以及相關的程式基礎構造進行完全的訪問,甚至允許我們動態構造額外的型別和程式基礎構造。這些靈活性的獲得當然伴隨有相當的空間(執行體所佔空間)和時間(程式執行效率)代價,但是它卻解決了日益增長的基於連線的計算環境中所面臨的問題和需要。

  最後,再來看第二個術語,即中間的斜線“ / ”,它往往為人們所忽略。其表示對 C++ 和 CLI 的一種繫結( binding ),它正是 C++/CLI 設計的焦點所在。據此,對於“什麼是 C++/CLI ”這一問題可能的一種答案便是“它是對靜態 C++ 物件模型和動態 CLI 元件模型的一種繫結”。

  對於 C++/CLI ,一個 C++ 程式設計師只需要將其新增到她 [ 譯註 1] 已有的程式設計工具箱中就可以了。要成為一個 C++/CLI 程式設計師,你無需放棄任何已有的東西,雖然你要步入一個新的技術世界,你仍然需要學習它——但願你能享受這一過程,至少我知道我是這樣的。由此觀之,我們還可以將 C++/CLI 看作是一扇通往另一個世界的大門。

  C++/CLI 將動態的、基於元件的程式設計模型和 ISO-C++ 整合在了一起,這種整合非常類似於我們當年在 Bell 實驗室對使用模板的泛型程式設計和當時的 C++ 所做的整合。在兩種情況下,你已有的程式碼投資和編碼經驗都將得到保留。這是我們設計 C++/CLI 時一個基本的需求。

  通用語言基礎構造( CLI )是一個多層的體系架構,它為所有 CLI 語言提供了各種各樣的服務。例如 CLI 中定義了一個通用型別系統( Common Type System ,簡稱 CTS ),而各個 CLI 語言都提供了自己對 CTS 的一個對映。該型別系統由一個根基類開始被組織為一個完整的類繼承體系。實際上,每一個 CLI 型別都是一個類——不僅包括像 integer 、 double 這樣的數值型別,而且也包括字面常量( literal constant )。每一個 CLI 型別(或者值)都表示一種 Object (所有 CLI 型別的根基類),比如數值 3.14159 、比如字串常量 "Homer Simpson" 。

單一的根基類為執行時型別查詢和程式碼生成(通常被稱為反射)提供了支援機制 [ 譯註 2] ,這是 ISO-C++ 所缺乏的。我們將在今後一系列文章中詳細討論它們給 CLI 帶來的動態程式設計特性。

  除此之外, CLI 還支援一種被稱作特性後設資料( attribute metadata )的構造,它允許我們定義一些特性類,然後將其關聯在 CLI 型別和當前正在執行的程式構造上——這有效地擴充套件了內建於 CLI 中的型別和程式構造。這些使用者定義的特性也可以透過反射機制來獲得,應用程式則可以根據它們的值來進行條件邏輯判斷。這也是 C++/CLI 為 C++ 帶來的動態元件程式設計的一部分。再次強調一遍,型別反射和特性將在我們的專欄中得到深入的討論。

  那麼,對於大家來說怎樣學習 C++/CLI 呢?學習 C++/CLI 的其中一個要點便是學習底層的通用型別系統( CTS ),它包括以下三種型別:

  1. 多型引用型別,其用於所有的類繼承。我們將在早期的一些專欄文章中討論它們。

  2. 非多型值型別,其用於實現一些類似於數值型別那樣的、對執行時效率要求比較高的型別。我們將其放在引用型別之後討論。

  3. 抽象介面型別,其用於定義一組供引用型別或者值型別實現的操作。介面為多繼承提供了一種別樣的設計模式。我們也將有一系列專欄文章來討論它們。

  將 CTS 對映為一組語言內建型別對於所有的 CLI 語言都適用,雖然各種語言所使用的語法各不相同。這也是一門 CLI 語言所要面對的第一個設計層面。例如,在 C# 中,我們可以用以下程式碼來定義一個抽象基型別 Shape (一些具體的幾何物件將繼承自它)。

  public abstract class Shape {…}

  而在 C++/CLI 中,我們用下面的程式碼來定義同樣的型別。

  public ref class Shape abstract {…};

  除了語法差異之外,兩種宣告的實際表示完全相同。類似地,在 C# 中,我們可以用下面的程式碼來定義一個具體類 Point2D 。

  public struct Point2D {…}

  而在 C++/CLI 中,我們用下面的程式碼來定義同樣的型別。

  public value class Point2D {…};

  我們對語法的選擇基於如下的出發點:以一種直觀的設計視角將 CLI 型別和 ISO-C++ 型別緊密地整合在一起。

  因此,簡單地說一種語言比另一種語言更接近底層 CLI 並不正確。相反,每一門 CLI 語言都只是表達了自己對底層 CLI 物件模型的一種檢視。

學習 C++/CLI 的第二個要點是學習我們選擇直接提供給程式設計師操作的那些底層 CLI 元素。例如, CLI 為所有語言都提供了垃圾收集服務。一門語言不能選擇是否支援垃圾收集,而只能選擇如何更好地提供該服務。
  在 CLI 中,一個引用型別的所有物件都只能被分配在 CLI 託管堆上。這意味著 C++/CLI 支援兩種動態堆——本地堆(沒有任何形式的自動記憶體回收機制),和 CLI 託管堆。對於這兩種動態堆,開發人員通常要用某種形式的 new 運算子來分配物件;如果操作成功,物件在堆中初始位置的地址將被返回。但是兩者又有所區別,這是因為 CLI 託管堆中物件的位置有可能在垃圾收集器的清除以及隨後的壓縮中被重新調整。如果一個物件的位置被重新調整,那麼 CLI 執行時中所含的其中一項服務會透明地更新所有引用該物件的指代品( thingee )。

  這就使得我們面臨著一種困難的選擇:我們是將這些指代品稱為指標,並且繼續用指標的語法來表示它們呢?還是引入一種新的類似的語法來表示它們需要特殊的處理?我們最後決定採用後者,看下面的程式碼:

  N *pn = new N;

  R ^rn = gcnew R;

  這裡, N 表示一個本地型別,而 R 表示一個 CLI 引用型別,帽子狀的符號( ^ )表示相關的地址是一個託管堆上的追蹤控制程式碼( tracking handle )——也就是說,物件位置的任何重新調整都會被 CLI 所追蹤,相應的控制程式碼也會被透明地更新。其中關鍵字 gcnew 在這裡被用作與 CLI 託管堆打交道的 new 表示式。

  值型別事實上也可以位於託管堆上,雖然這並非必須。當它們作為一個引用型別的成員時,就會出現這種情況。如果我們允許獲取一個引用型別內部成員的地址,那麼本地指標也是不合適的,因為這些成員的位置也需要被追蹤。一種解決方法是簡單地禁止該項功能。這樣語言當然會變得更加簡單,但是同時語言也會變得更弱——例如我們將不能透過增長元素的地址值來遍歷 CLI 陣列,這是因為 CLI 陣列是一個引用型別,其內的元素都位於託管堆上。不提供這樣的功能意味著 CLI 陣列將不能適用於標準模板庫( STL )中的 iterator 模式以及泛型演算法。對於一個 C++ 程式設計師來說,這是不可接受的。

  支援獲取可能位於託管堆中的值型別的地址同樣需要引入一種追蹤指標,我們稱之為追蹤內部指標( tracking interior pointer )。另外,我們還支援追蹤引用( tracking reference )這樣的概念——它具有類似本地引用的別名語義,但是它會在必要的時候被 CLI 透明地更新。最後,我們還支援一種固定指標( pinning pointer )的概念,它可以在該指標的作用範圍內阻止垃圾收集器移動其所引用的物件。

  這些新的符號及其表示的複雜的間接型別是在我們對託管堆反覆學習和認識之後產生的。面對生存期短暫的託管堆物件,我們需要某種精巧的方式來認識和使用它們,我們相信這些額外的間接型別可以給大家很多幫助。我們將在今後的專欄文章中詳細討論它們。

  我們在此對一門 CLI 語言所選擇的第二個設計層面表示了其對底層 CLI 實現模型的一層對映。選擇什麼樣的對映取決於該程式語言定位於什麼樣的程式及程式設計師模型。當你選擇一門 CLI 語言進行程式設計的時候,你實際上也是在選擇遵從一種程式設計師模型。我們對於 C++/CLI 程式設計師的定位是那些歷練較深的系統程式設計師,這些程式設計師通常所面對的任務是為高層的商業邏輯提供基礎性的構造和關鍵性的應用,這時候她就必須要同時考慮系統的擴充套件性和效能,因此必須對底層 CLI 有一個系統級的視角。

學習 C++/CLI 的第三個要點是學習那些非 CLI 本身所直接提供的功能特性。這也是每一門面向 CLI 的語言所要面對的設計選擇,也是各種 CLI 語言之間相互區分的一種體現。

  例如, CLI 本身並不支援多類繼承 (multiple class inheritance ,簡稱 MCI) ,而只支援多介面繼承和單類繼承。但 Eiffel 語言在設計其面向 CLI 的實現時就選擇了支援原始碼級的多類繼承。這需要一種巧妙、甚至是複雜的設計將原始碼級的多類繼承對映為底層 CLI 的單類繼承模型。 Eiffel 語言的設計人員認為這種對映對於 CLI 平臺上的 Eiffel 程式設計師是一個利好的元素。

  在此 C++/CLI 的第三個設計層面上,我們沒有采用多類繼承的方案。其中一個原因是我們不能說服自己多介面繼承模型有任何不夠簡單或者優雅的地方。我們沒有足夠的經驗來確定哪種方案絕對的優秀,但是我的直覺告訴我多類繼承( MCI )是一個死衚衕。我們在此設計層面上的主要關注點在於為那些 CLI 本身所欠缺的地方提供一些額外的解決方案,我們主要集中在以下三個方面:

  1. 為某些 CLI 要求手動干預的地方提供一種自動化的解決方案,例如確定性終止化操作( deterministic finalization )和稀有資源釋放。

  2. 提供一些特殊的類成員函式——例如複製構造器和複製賦值運算子,以及在 CLI 直接支援的運算子的基礎上再為一些運算子提供一些擴充套件支援——例如用來支援函式物件( function object )設計模式的呼叫運算子“()”。

  3. 提供一種靜態的引數化機制來支援設計適用於 CLI 型別的標準模板庫( STL ),這是因為 CLI 中的泛型機制在我們來看對於當代的引數化設計是不夠的——雖然我們也支援它們。

  以上幾點在我們的系列專欄中都將有相關的討論。特別地,我們將會詳細闡釋 C++/CLI 中的模板和泛型機制。

  C++/CLI 的第四個設計層面在於它選擇了“整合”而非“替換”的策略,這是 C++ 以及一些語言所獨有的,而其他一些語言則沒有這樣做,例如 Visual Basic 採取的就是“替換”的策略。一個合法的 C++ 程式是可以順利透過 C++/CLI 編譯,並且可以正常執行的。我們認為這對於我們的程式設計師是一項基本的需求。

  談到 C++/CLI 的第四個設計層面,這究竟是什麼意思呢?它表示我們對 C++/CLI 語言規範和 ISO-C++ 所做的深入的整合。例如,除了我們擴充套件支援集合使其也適用於統一的 CLI 型別系統,表示式評估的標準轉換集合與過載函式的辨析都和 ISO-C++ 的相同。當我們引入模板和多繼承機制時,我們也應用了同樣的擴充套件策略。這些都是在語言中稍顯抽象的部分,在某種程度上我們已經使它們的行為變得更加直觀,免除了程式設計師深入演算法細節的需要。但我們仍會在系列專欄中花費筆墨關注一些主要的變化,例如對字面常量( literal )字串的處理。

  在 C++/CLI 未來的版本中,我們希望為本地型別和 CLI 型別提供更為無縫的整合。在目前的實現中,仍然存在許多不能跨越的壁壘。例如,我們現在還不能直接在一個 CLI 類中宣告一個本地類的例項物件;相反,我們必須宣告一個指向那個本地物件的指標,然後在 CLI 類的構造器 / 析構器對中處理對它的記憶體分配與釋放。我們希望將來能夠透明地處理它們。類似地,如果可以方便地編寫下面的程式碼就更好了:

N^ n = gcnew N;

  R* pn = new R;

  即將一個本地類透明地放在垃圾收集控制的託管堆中,以及將一個 CLI 引用型別透明地放在本地堆中,並使它們正常執行。這些是我們對於 C++/CLI 未來的一些設想和願景。隨著這些設想的實現,我們也會在我們的專欄中討論它們。

  最後,再回答一個大家經常問到的一個問題,“我為什麼要學習 C++/CLI ”?首要的原因是 C++/CLI 將會為你進入 CLI 所表示的動態元件程式設計模型領域提供一張第一等的入口簽證。如果你像我一樣認為這將成為越來越重要的一種程式設計模型,並且如果你是一個歷練較深的程式設計師,那麼 C++/CLI 就是你想要的一個語言工具。如果你不喜歡某些地方,或者發現某些東西很難表達,那麼請告訴我們。我們代表著一個動態程式設計社群, C++/CLI 也會持續不斷地前進。

  在 C++/CLI 之前,如果我們希望或者需要在 CLI 所表示的動態程式設計領域工作,那麼我們只能放棄使用 C++ [ 譯註 3] ,這意味著我們同時放棄了我們現存的程式碼庫和編碼經驗。有了 C++/CLI 之後,我們就擁有了一條沿著 C++ 向上的移植路徑。這是學習 C++/CLI 的第一個原因。

  學習 C++/CLI 的第二個原因在於它允許我們訪問整個 CLI 框架類庫,包括使用者介面,執行緒,網路, XML , ADO.NET , ASP.NET ,以及 Web 服務這個寬廣誘人的世界。另外,在即將推出的 WinFX 中,一個封裝了整個作業系統的類庫體系(包括應用程式及其執行空間 [ 譯註 4] )也會被收編在 CLI 門下。

  [ 譯註 1 ] :在翻譯 Stan Lippman 先生這篇文章的過程中,我發現 Stan 在遇到第三人稱的程式設計師時,總是使用“ She ”、“ Her ”這樣的女性代詞,一開始我很困惑,因為感覺很不符合閱讀習慣,但我總覺得 Stan 是有意為之。最後我決定向 Stan 詢問這樣做的用意。果不其然, Stan 的回答是大家習慣用“ He ”是一種男性至上主義者的體現,好像一提起程式設計師,大家都認為是男性。他並不認同這樣的看法,特意囑我要在翻譯的文字中保留“ She ”和“ Her ”的用意,因為他反對那種老套的觀點。同時還舉出了兩位計算機領域的女傑:軟體界的先驅之一、組合語言的創始人 Grace Hopper 博士,以及 Smalltalk 領域的重量級專家 Adele Goldberg 女士。希望 Stan 的良苦用心能夠鼓勵更多的女性程式設計師朋友來閱讀我們這個專欄 J ? back

  [ 譯註 2 ] :單一的根基類為反射提供支援機制的理由在於反射總需要某種形式的 handle 來維護型別資訊。比如在 ISO-C++ 中,這樣的 handle 需要虛表來支援,如果沒有虛表,就不能支援 RTTI ,這使得 ISO-C++ 對反射的支援比較弱。但 CLI 採用在一個公共的 object header 中放入一個 handle 來維護型別資訊,巧妙地解決了執行時型別發現的問題。這個公共的 object header 最後就會導致所有的型別都有一個根基類——如果不是刻意隱藏該根基類的話。 back

  [ 譯註 3 ] :作者這裡沒有考慮 Managed C++ 是因為 C++/CLI 是 Managed C++ 的第二版。 back

  [ 譯註 4 ] :這裡的“執行空間”指的是應用程式執行時的一些基礎構造,如程式集、應用程式域等。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10617731/viewspace-957361/,如需轉載,請註明出處,否則將追究法律責任。

相關文章