• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>
            posts - 17,  comments - 2,  trackbacks - 0
            轉(zhuǎn)換指南: 將程序從托管擴展 C++ 遷移到 C++/CLI
            發(fā)布日期 : 5/31/2005 | 更新日期 : 5/31/2005

            Stanley B. Lippman 
            Microsoft Corporation

            適用于:
            C++/CLI 第二版
            ISO-C++

            摘要 :C++/CLI代表 ISO-C++標準語言的一個動態(tài)編程范型擴展。本文列舉了 V1 版本語言的功能,以及它們在 V2 版本語言中的對應(yīng)功能(如果存在);并指出了不存在相應(yīng)功能的那些構(gòu)造。

            本頁內(nèi)容

            簡介 簡介 
            1. 語言關(guān)鍵字 1. 語言關(guān)鍵字 
            2. 托管類型 2. 托管類型 
            3.類或接口中的成員聲明 3.類或接口中的成員聲明 
            4 值類型及其行為 4 值類型及其行為 
            5. 語言變化概要 5. 語言變化概要 
            附錄:推動修訂版語言設(shè)計 附錄:推動修訂版語言設(shè)計 
            致謝 致謝 

            簡介

            C++/CLI代表 ISO-C++標準語言的一個動態(tài)編程泛型擴展 (dynamic programming paradigm extension)。在原版語言設(shè)計 (V1) 中有許多顯著的弱點,我們覺得在修訂版語言設(shè)計 (V2) 中已經(jīng)修正了這些弱點。本文列舉了 V1 版本語言的功能和它們在 V2 版本中的對應(yīng)功能(如果存在);并指出了其對應(yīng)功能不存在的構(gòu)造。對于有興趣的讀者,可以查看附錄中提供新語言設(shè)計的擴展原理。另外,一個源代碼級別的轉(zhuǎn)換工具 (mscfront) 正在開發(fā)中,而且可能在 C++/CLI的發(fā)布版中提供給希望將 V1 代碼自動移植到新語言設(shè)計的人。

            本文分為五個章節(jié)加一個附錄。第一節(jié)討論語言關(guān)鍵字的主要問題,特別是雙下劃線的移除以及與上下文相關(guān)和由空格分隔的關(guān)鍵字。第二節(jié)著眼于托管類型的變化 — 特別是托管引用類型和數(shù)組。還可以在這里找到有關(guān)確定性終結(jié)語義 (deterministic finalization) 的詳細討論。關(guān)于類成員的變化,例如屬性、索引屬性和操作符,是第三節(jié)的重點。第四節(jié)著眼于 CLI 枚舉、內(nèi)部和釘住指針的語法變化。它也討論了許多可觀的語義變化,例如隱式裝箱的引入、CLI枚舉的變化,和對類中默認構(gòu)造函數(shù)的支持的移除。第五節(jié)有點像大雜燴 — 亂七八糟的雜項。討論了類型轉(zhuǎn)換符號、字符串字符的行為和參數(shù)數(shù)組。

            1. 語言關(guān)鍵字

            原版到修訂版語言設(shè)計的一個重要轉(zhuǎn)換是在所有關(guān)鍵字中去掉雙下劃線。舉例來說,一個屬性現(xiàn)在被聲明為 property 而不是 __property。在原版語言設(shè)計中使用雙下劃線前綴的兩個主要原因是:

            1. 這是提供符合 ISO-C++標準的本地擴展的一致性方法。原版語言設(shè)計的一個主要目標就是不引入與標準語言的不兼容性,例如新的關(guān)鍵字和標記。這個原因很大程度上也推動了對聲明托管引用類型的對象的指針語法的選擇。

            2. 雙下劃線的使用,除了兼容性方面的原因之外,也是一個不會對有舊代碼基礎(chǔ)的用戶造成影響的合理保證。這是原版語言設(shè)計的第二主要目標。

            這樣的話,為什么我們移除雙下劃線(并且引入了一些新的標記)?不是的,這并不代表我們不再考慮和標準保持一致!

            我們繼續(xù)致力于和標準一致。盡管如此,我們意識到對 CLI動態(tài)對象模型的支持表現(xiàn)出了一種全新的強大的編程范型。我們在原版語言設(shè)計上的經(jīng)驗以及設(shè)計與發(fā)展 C++ 語言本身的經(jīng)驗使我們確信,對這個新范型的支持需要它自己的高級關(guān)鍵字和標記。我們想提供一個該新范型的一流表達方式,整合它并且支持標準語言。我們希望您會感受到修訂版語言設(shè)計提供了對這兩種截然不同的對象模型的一流的編程體驗。

            類似的,我們很關(guān)心最小化這些新的關(guān)鍵字的對現(xiàn)有代碼可能造成的沖擊。這是用與上下文相關(guān)和由空格分隔的關(guān)鍵字來解決的。在我們著眼于實際語言語法的修訂之前,讓我們試試搞清楚這兩個特別關(guān)鍵字的特點。

            一個與上下文相關(guān)的關(guān)鍵字在特定的程序上下文中有特殊的含義。例如,在通常的程序中,sealed 是一個普通標識符。但是,在一個托管引用類類型的聲明部分,它就是類聲明上下文中的一個關(guān)鍵字。這使得在語言中引入一個新的關(guān)鍵字的潛在影響降到最低程度,我們認為,這對已經(jīng)擁有代碼基的用戶非常重要。同時,它允許新功能的使用者獲得一流的新增語言功能的體驗 — 我們認為在原版語言設(shè)計中缺少這些因素。我們將在 2.1.2節(jié)中看到 sealed 用法的示例。

            一個由空格分隔的關(guān)鍵字是與上下文相關(guān)關(guān)鍵字的特例。它在字面上將一個與上下文相關(guān)的修飾符和一個現(xiàn)存的關(guān)鍵字配對,用空格分隔。這個配對作為一個單獨的單位,例如 value class(示例參見 1.1 節(jié)),而不是兩個單獨的關(guān)鍵字?;诂F(xiàn)實的因素,這意味著一個重新定義 value 的宏,如下所示:

               #ifndef __cplusplus_cli 
               #define value 
            

            不會在一個類聲明中去掉 value。如果確實要這么做的話,必須重新定義單元對,編寫如下代碼:

               #ifndef __cplusplus_cli 
               #define value class class 
            

            考慮到現(xiàn)實的因素,這是十分必要的。否則,現(xiàn)存的 #define 可能轉(zhuǎn)換由空格分隔的關(guān)鍵字的與上下文相關(guān)的關(guān)鍵字部分。

            2. 托管類型

            聲明托管類型和創(chuàng)建以及使用這些類型的對象的語法已經(jīng)大加修改,以提高 ISO-C++類型系統(tǒng)內(nèi)的集成性。這些更改在后面的小節(jié)中詳述。委托的討論延后到 2.3節(jié)以用類中的事件成員表述它們 — 這是第 2 節(jié)的主題。(有關(guān)更加詳細的跟蹤引用語法介紹的內(nèi)幕和設(shè)計上的主要轉(zhuǎn)變的討論,請參見附錄A:推動修訂版語言設(shè)計。)

            2.1 聲明一個托管類類型

            在原版語言定義中,一個引用類類型以 __gc關(guān)鍵字開頭。在修訂版語言中,__gc關(guān)鍵字被兩個由空格分隔的關(guān)鍵字 ref class或者 ref struct之一替代。struct或者 class的選擇只是指明在類型體中開頭未標記部分聲明的其成員的公共(對于 struct)或者私有(對于 class)默認訪問級別。

            類似地,在原版語言定義中,一個 value 類類型以 __value 關(guān)鍵字開頭。在修訂版語言中,__value 關(guān)鍵字被兩個由空格分隔的關(guān)鍵字 value class或者 value struct之一代替。

            在原版語言設(shè)計中,一個接口類型是用關(guān)鍵字 __interface指明的。在修訂版語言中,它被 interface class替代。

            例如,下列類聲明對

               // 原版語法  
               public __gc class Block { ... };   // 引用類 
               public __value class Vector { ... };   // 值類 
               public __interface IMyFile { ... };   // 接口類 
            在修訂版語言設(shè)計下等價的聲明如下: 
               // 修訂版語法  
               public ref class Block { ... }; 
               public value class Vector { ... }; 
               public interface class IMyFile { ... }; 
            

            選擇 ref(對于引用類型)而不是 gc(對于垃圾收集類型)是為了便于更好地暗示這個類型的本質(zhì)。

            2.1.1 指定一個類為抽象類型

            在原版語言定義中,關(guān)鍵字 __abstract放在類型關(guān)鍵字之前(__gc之前或者之后)以指明該類尚未完成,而且此類的對象不能在程序中創(chuàng)建:

               public __gc __abstract class Shape {}; 
               public __gc __abstract class Shape2D: public Shape {}; 
            

            在修訂版語言設(shè)計中,abstract 與上下文相關(guān)的關(guān)鍵字被限定在類名之后,類體、基類派生列表或者分號之前。

               public ref class Shape abstract {}; 
               public ref class Shape2D abstract : public Shape{}; 
            

            當然,語義沒有變化。

            2.1.2 指定一個類為密封類型

            在原版語言定義中,關(guān)鍵字 __sealed放在 class 關(guān)鍵字之前(__gc之前或者之后)以指明類的對象不能從以下類繼承:

               public __gc __sealed class String {}; 
            

            在 V2語言設(shè)計中,與上下文相關(guān)的抽象關(guān)鍵字限定在類名之后,類體、基類派生列表或者分號之前(您可以聲明一個繼承類并密封它。舉例來說,String類隱式派生自 Object)。密封一個類的好處是允許靜態(tài)(即在編譯時)解析這個密封引用類對象的所有的虛函數(shù)調(diào)用。這是因為密封指示符保證了 String 跟蹤句柄不能指向一個可能重載被調(diào)用的虛方法實例的派生類。

               public ref class String sealed {}; 
            

            也可以將一個類既聲明為抽象類也聲明為密封類。這是一種被稱為靜態(tài)類的特殊情況。這在CLI文檔中描述如下:

            同時為抽象和密封的類型只能有靜態(tài)成員,并且以一些語言中調(diào)用命名空間一樣的方式服務(wù)。

            例如,以下是一個使用 V1語法的抽象密封類的聲明

               public __gc __sealed __abstract class State  
            { 
            public: 
               static State(); 
               static bool inParamList(); 
            private: 
               static bool ms_inParam; 
            }; 
            而以下是在修訂版語言設(shè)計中的聲明: 
               public ref class State abstract sealed 
            { 
            public: 
               static State(); 
               static bool inParamList(); 
            private: 
               static bool ms_inParam; 
            }; 
            

            2.1.3 CLI 繼承 指定基類

            在 CLI對象模型中,只支持公有方式的單繼承。但是,在原始語言定義中仍然保留了ISO-C++對基類的默認解釋,而無需訪問關(guān)鍵字指定私有派生。這意味著每一個 CLI繼承聲明必須用一個 public關(guān)鍵字來代替默認的解釋。很多用戶認為編譯器似乎過于嚴謹。

            // V1:錯誤:默認為私有派生 
            __gc class My : File{}; 
            

            在修訂版語言定義中,CLI繼承定義缺少訪問關(guān)鍵字時,默認是以公有的方式派生。這樣,公有訪問關(guān)鍵字就不再必要,而是可選的。雖然這個改變不需要對 V1的代碼做任何的修改,出于完整性考慮我仍將這個變化列出。

            // V2:正確:默認是公有性派生 
            ref class My : File{}; 
            

            2.2 一個 CLI 的引用類對象的聲明

            在原版語言定義中,一個引用類類型對象是使用 ISO-C++指針語法聲明的,在星號左邊使用可選的 __gc關(guān)鍵字。例如,以下是 V1語法下多種引用類類型對象的聲明:

            public __gc class Form1 : public System::Windows::Forms::Form { 
            private: 
               System::ComponentModel::Container __gc *components; 
               Button __gc *button1; 
               DataGrid __gc *myDataGrid;    
               DataSet __gc *myDataSet; 
            void PrintValues( Array* myArr )   
            { 
                System::Collections::IEnumerator* myEnumerator =  
            myArr->GetEnumerator(); 
                      Array *localArray = myArr->Copy(); 
                      // ... 
                  } 
               }; 
            

            在修訂版語言設(shè)計中,引用類類型的對象用一個新的聲明性符號(^)聲明,正式的表述為跟蹤句柄,不正式的表述為帽子。(跟蹤這個形容詞強調(diào)了引用類型對象位于 CLI堆中,因此可以透明地在垃圾回收堆的壓縮過程中移動它的位置。一個跟蹤句柄在運行時被透明地更新。兩個類似的概念:(a)跟蹤引用(%) 和 (b)內(nèi)部指針(interior_ptr<>),在第4.4.3節(jié)討論。

            聲明語法不再重用 ISO-C++指針語法有兩個主要原因:

            1. 指針語法的使用不允許重載的操作符直接應(yīng)用于引用對象;而必須通過其內(nèi)部名稱調(diào)用操作符,例如 rV1->op_Addition (rV2) 而不是更加直觀的 rV2+Rv2。

            2. 有許多指針操作,例如類型強制轉(zhuǎn)換和指針算術(shù)對于位于垃圾回收堆上的對象無效。我們認為一個跟蹤句柄的概念最好符合一個 CLI 引用類型的本性。

            對一個跟蹤句柄使用 __gc修飾符是不必要的,而且是不被支持的。對象本身的用法并未變化,它仍舊通過指針成員選擇操作符 (->) 訪問成員。例如,以下是上面的 V1文字轉(zhuǎn)換到新語言語法的結(jié)果:

            public ref class Form1: public System::Windows::Forms::Form{ 
            private: 
               System::ComponentModel::Container^ components; 
               Button^ button1; 
               DataGrid^ myDataGrid; 
               DataSet^ myDataSet; 
            void PrintValues( Array^ myArr ) 
            { 
                      System::Collections::IEnumerator^ myEnumerator = 
                            myArr->GetEnumerator(); 
                      Array ^localArray = myArr->Copy(); 
                         // ... 
                  } 
               }; 
            

            2.2.1  CLI 堆上動態(tài)分配對象

            在原版語言設(shè)計中,現(xiàn)有的在本機堆和托管堆上分配的兩種 new表達式很大程度上是透明的。在幾乎所有的情況下,編譯器能夠從上下文正確地確定所需的是本機堆還是托管堆。例如:

               Button *button1 = new Button; // OK: 托管堆 
               int *pi1 = new int;           // OK: 本機堆 
               Int32 *pi2 = new Int32;       // OK: 托管堆 
            

            在上下文堆分配并非所期望的實例時,可以用 __gc或者 __nogc關(guān)鍵字指引編譯器。在修訂版語言中,使用新引入的 gcnew關(guān)鍵字來顯示兩個 new 表達式的不同本質(zhì)。例如,上面三個聲明在修訂版語言中如下所示:

            Button^ button1 = gcnew Button;        // OK: 托管堆 
            int * pi1 = new int;                   // OK: 本機堆 
            interior_ptr<Int32> pi2 = gcnew Int32; // OK: 托管堆 
            

            (在第 3 節(jié)中討論 interior_ptr的更多細節(jié)。通常,它表示一個對象的地址,這個對象可能(但不必)位于托管堆上。如果指向的對象確實位于托管堆上,那么它在對象被重新定位時被透明地更新。)

            以下是前面一節(jié)中聲明的 Form1成員 V1版本的初始化:

            void InitializeComponent()  
            { 
                  components = new System::ComponentModel::Container(); 
                  button1 = new System::Windows::Forms::Button(); 
                  myDataGrid = new DataGrid(); 
                  button1->Click +=  
                        new System::EventHandler(this, &Form1::button1_Click); 
                  // ... 
            } 
            

            以下是用修訂版語法重寫的同樣的初始化過程,注意引用類型是一個 gcnew表達式的目標時不需要“帽子”。

               void InitializeComponent() 
               { 
                  components = gcnew System::ComponentModel::Container; 
                  button1 = gcnew System::Windows::Forms::Button; 
                  myDataGrid = gcnew DataGrid; 
                  button1->Click +=  
                        gcnew System::EventHandler( this, &Form1::button1_Click ); 
                  // ... 
               } 
            

            2.2.2 無對象的跟蹤引用

            在新的語言設(shè)計中,0不再表示一個空地址,而僅被處理為一個整型,與 1、10、100一樣,這樣我們需要引入一個特殊的標記來代表一個空值的跟蹤引用。例如,在原版語言設(shè)計中,我們?nèi)缦鲁跏蓟粋€引用類型來處理一個無對象:

            //正確:我們設(shè)置 obj 不引用任何對象 
            Object * obj = 0; 
            //錯誤:沒有隱式裝箱 
            Object * obj2 = 1; 
            

            在修訂版語言中,任何從值類型到一個 Object的初始化或者賦值都導致一個值類型的隱式裝箱。在修訂版語言中,obj和 obj2都被初始化為裝箱過的 Int32對象,分別具有值 0和 1。例如:

            //導致 0 和 1 的隱式裝箱 
            Object ^ obj = 0; 
            Object ^ obj2 = 1; 
            

            因此,為了允許顯式的初始化、賦值,以及將跟蹤句柄與空進行比較,我們引入了一個新的關(guān)鍵字 nullptr。這樣 V1示例的正確版本如下所示:

            //OK:我們設(shè)置 obj 不引用任何對象 
            Object ^ obj = nullptr; 
            //OK:我們初始化 obj 為一個 Int32^ 
            Object ^ obj2 = 1; 
            

            這使得從現(xiàn)存 V1代碼到修訂版語言設(shè)計的移植更加復雜。例如,考慮如下類聲明:

            __value struct Holder { //原版 V1 語法 
                  Holder( Continuation* c, Sexpr* v ) 
                  { 
                     cont = c; 
                     value = v; 
                     args = 0; 
                     env = 0; 
               } 
            private: 
               Continuation* cont; 
               Sexpr * value; 
               Environment* env; 
               Sexpr * args __gc []; 
               }; 
            

            這里 args env都是 CLI引用類型。在構(gòu)造函數(shù)中將這兩個成員初始化為 0 語句在轉(zhuǎn)移到新語法的過程中必須修改為 nullptr

            //修訂版 V2 語法 
            value struct Holder 
            { 
               Holder( Continuation^ c, Sexpr^ v ) 
               { 
                  cont = c; 
                  value = v; 
                  args = nullptr; 
                  env = nullptr; 
               } 
            private: 
               Continuation^ cont; 
               Sexpr^ value; 
               Environment^ env; 
               array<Sexpr^>^ args; 
            }; 
            

            類似的,將這些成員與 0進行比較的測試也必須改為和 nullptr比較。以下是原版的語法:

            // 原版 V1 語法 
            Sexpr * Loop (Sexpr* input) 
               { 
                  value = 0; 
                  Holder holder = Interpret(this, input, env); 
                  while (holder.cont != 0) 
                  { 
                  if (holder.env != 0) 
                  { 
                    holder=Interpret(holder.cont,holder.value,holder.env); 
                  } 
                  else if (holder.args != 0) 
                  { 
                    holder =  
                holder.value->closure()-> 
                              apply(holder.cont,holder.args); 
                  } 
                  } 
                  return value; 
               } 
            

            而以下是修訂版語法。將每個 0實例轉(zhuǎn)換為 nullptr 。(轉(zhuǎn)換工具有助于這個轉(zhuǎn)換,進行許多自動處理 — 如果不是全部出現(xiàn),包括使用 NULL 宏。

            //修訂版 V2 語法 
            Sexpr ^ Loop (Sexpr^ input) 
               { 
                  value = nullptr; 
                  Holder holder = Interpret(this, input, env); 
                  while ( holder.cont != nullptr ) 
                  { 
                  if ( holder.env != nullptr ) 
                  { 
                    holder=Interpret(holder.cont,holder.value,holder.env); 
                  } 
                  else if (holder.args != nullptr ) 
                  { 
                    holder =  
                holder.value->closure()-> 
                              apply(holder.cont,holder.args); 
                  } 
                  } 
                  return value; 
               } 
            

            nullptr可以轉(zhuǎn)化成任何跟蹤句柄類型或者指針,但是不能提升為一個整數(shù)類型。例如,在如下初始化集合中,nullptr只在開頭兩個初始值中有效。

            //正確:我們設(shè)置 obj 和 pstr 不引用任何對象 
            Object^ obj = nullptr; 
            char*   pstr = nullptr; //在這里用0也可以 
            //錯誤:沒有從 nullptr 到 0 的轉(zhuǎn)換 ... 
            int ival = nullptr; 
            類似的,給定一個重載過的方法集,如下所示: 
            void f( Object^ ); // (1) 
            void f( char* );   // (2) 
            void f( int );     // (3) 
            

            一段使用 nullptr的調(diào)用如下所示:

            // 錯誤:歧義:匹配 (1) 和 (2) 
            f(  nullptr ); 
            

            是有歧義的,因為 nullptr既匹配一個跟蹤句柄也匹配一個指針,而且在兩者中沒有一個優(yōu)先選擇(這需要一個顯式的類型強制轉(zhuǎn)換來消除歧義)。

            一個使用 0的調(diào)用正好匹配實例 (3):

            //正確:匹配 (3) 
            f( 0 ); 
            

            由于 0整型。當沒有 f(int)的時候,調(diào)用會通過一個標準轉(zhuǎn)換無歧義地匹配f(char*)。匹配規(guī)則優(yōu)先于標準轉(zhuǎn)換的精確匹配。在沒有精確匹配時,標準轉(zhuǎn)換優(yōu)先于對于類型的隱式裝箱。這就是沒有歧義的原因。

            2.3 CLI 數(shù)組的聲明

            原版語言設(shè)計中的 CLI數(shù)組對象的聲明是標準數(shù)組聲明的有點不直觀的擴展,其中,一個 __gc關(guān)鍵字放在數(shù)組對象名和可能的逗號填充的維數(shù)之間,如下一對示例所示:

            // V1 語法 
            void PrintValues( Object* myArr __gc[]); 
            void PrintValues( int myArr __gc[,,]); 
            

            這在修訂版語言設(shè)計中被簡化了,其中,我們使用一個類似于模板的聲明,它說明了STL 向量聲明。第一個參數(shù)指定元素類型。第二個參數(shù)指定數(shù)組維數(shù)(默認值是 1,所以只有多維數(shù)組才需要第二個參數(shù))。數(shù)組對象本身是一個跟蹤句柄,所以必須給它一個帽子。如果元素類型也是一個引用類型,那么,它們也必須被標記。例如,上面的示例,在修訂版語言中表達時如下所示:

            // V2 語法 
            void PrintValues( array<Object^>^ myArr ); 
            void PrintValues( array<int,3>^ myArr ); 
            

            因為引用類型是一個跟蹤句柄而不是一個對象,所以可能將一個 CLI數(shù)組類型用于函數(shù)的返回值類型(本機數(shù)組不能用作函數(shù)返回值)。在原版語言設(shè)計中,其語法也有點不直觀。例如:

            // V1 語法 
            Int32 f() []; 
            int GetArray() __gc[]; 
            

            在 V2中,這個聲明閱讀和分析起來簡單多了。例如:

            // V2 語法 
            array<Int32>^ f(); 
            array<int>^ GetArray(); 
            

            本地托管數(shù)組的快捷初始化在兩種版本的語言中都支持。例如

            // V1 語法 
            int GetArray() __gc[] 
            { 
               int a1 __gc[] = { 1, 2, 3, 4, 5 }; 
               Object* myObjArray __gc[] = {  
            __box(26), __box(27), __box(28), __box(29), __box(30) 
               }; 
               // ... 
            } 
            

            在 V2中被大大簡化了(注意因為修訂版語言設(shè)計中的裝箱是隱式的,__box操作符被去掉了— 關(guān)于其討論參見第 3 節(jié)。

            // V2 語法 
            array<int>^ GetArray() 
            { 
               array<int>^ a1 = {1,2,3,4,5}; 
            array<Object^>^ myObjArray = {26,27,28,29,30}; 
            // ... 
            } 
            

            因為數(shù)組是一個 CLI引用類型,每個數(shù)組對象的聲明都是一個跟蹤句柄。因此,它必須在CLI堆上被分配(快捷符號隱藏了在托管堆上進行分配的細節(jié))。以下是原版語言設(shè)計中一個數(shù)組對象的顯式初始化形式:

            // V1 語法 
            Object* myArray[] = new Object*[2]; 
            String* myMat[,] = new String*[4,4]; 
            

            回憶一下,在新的語言設(shè)計中,new表達式被 gcnew替代了。數(shù)組的維大小作為參數(shù)傳遞給 gcnew表達式,如下所示:

            // V2 語法 
            array<Object^>^ myArray = gcnew array<Object^>(2); 
            array<String^,2>^ myMat = gcnew array<String^,2>(4,4); 
            

            在修訂版語言中,gcnew表達式后面可以跟一個顯式的初始化列表,這在 V1語言中不被支持,例如:

            // V2 語法  
            // explicit initialization list follow gcnew  
            //          is not supported in V1 
            array<Object^>^ myArray =  
                  gcnew array<Object^>(4){ 1, 1, 2, 3 } 
            

            2.4 析構(gòu)函數(shù)語義的變化

            在原版語言定義中,類的析構(gòu)函數(shù)允許存在于引用類中,但是不允許存在于類中。這在修訂的 V2語言設(shè)計中沒有變化。但是,類析構(gòu)函數(shù)的語義有可觀的變化。怎樣和為什么變化(以及這會對現(xiàn)存 V1代碼的轉(zhuǎn)換造成怎樣的影響)是本節(jié)的主題。這可能是本文中最復雜的一節(jié),所以我們慢慢來講。這也可能是兩個語言版本之間最重要的編程級別的修改,所以需要以循序漸進的方式來進行學習。

            2.4.1 不確定的終止

            對象關(guān)聯(lián)的內(nèi)存被垃圾回收器回收之前,如果對象有一個相關(guān)的 Finalize()方法存在,那么它將被調(diào)用。您可以將該方法想象為一種超級析構(gòu)函數(shù),因為它與對象編程生命周期無關(guān)。我們稱此為終止。何時甚至是否調(diào)用 Finalize()方法的計時是不確定的。這就是我們提到垃圾回收代表不確定的終止(non-deterministic finalization)時表達的意思。

            不確定的終止和動態(tài)內(nèi)存管理合作的很好。當可用內(nèi)存缺少到一定程度的時候,垃圾回收器介入,并且很好地工作。在垃圾回收環(huán)境中,用析構(gòu)函數(shù)來釋放內(nèi)存是不必要的。您第一次實現(xiàn)應(yīng)用程序時不為潛在的內(nèi)存泄漏發(fā)愁才怪,但是很容易就會適應(yīng)了。

            然而,不確定的終止機制在對象維護一個關(guān)鍵的資源(例如一個數(shù)據(jù)庫連接或者某種類型的鎖)時運轉(zhuǎn)并不好。這種情況下我們需要盡快釋放資源。在本機代碼的環(huán)境下,這是用構(gòu)造函數(shù)/析構(gòu)函數(shù)對的組合解決的。不管是通過執(zhí)行完畢聲明對象的本機代碼塊還是通過由于引發(fā)異常造成的拆棧,對象的生命周期一終止,析構(gòu)函數(shù)就介入并且自動釋放資源。這個機制運轉(zhuǎn)得很好,而且在原版語言設(shè)計中沒有它的存在是一個很大的失誤。

            CLI提供的解決方案是實現(xiàn) IDisposable接口的 Dispose()方法的類。問題是Dispose()方法需要用戶顯式地調(diào)用。這是個錯誤的傾向,因此是個倒退。C# 語言提供一個適度的自動化方式,使用一個特別的 using語句。我們的原版語言設(shè)計(我已經(jīng)提到過)根本沒有提供特別的支持。

            2.4.2  V1 中,析構(gòu)函數(shù)轉(zhuǎn)到 Finalize()

            在原版語言中,一個引用類的析構(gòu)函數(shù)通過如下兩步實現(xiàn):

            1. 用戶編寫的析構(gòu)函數(shù)被內(nèi)部重命名為 Finalize()。如果類有一個基類(記住,在 CLI對象模型中只支持單繼承),編譯器在用戶的代碼之后插入一個對其終結(jié)器的調(diào)用。例如,給定下列 V1語言規(guī)范中的普通層次

              __gc class A { 
              public: 
                 ~A() { Console::WriteLine(S"in ~A"); } 
              }; 
              __gc class B : public A { 
              public: 
                 ~B() { Console::WriteLine(S"in ~B");  } 
              }; 
              

              兩個析構(gòu)函數(shù)都被重命名為 Finalize()B  Finalize()在調(diào)用 WriteLine()之后加入一個 A Finalize()方法的調(diào)用。這些就是垃圾回收器在終止過程中默認調(diào)用的代碼。它的內(nèi)部轉(zhuǎn)換結(jié)果如下所示:

              //V1 下析構(gòu)函數(shù)的內(nèi)部轉(zhuǎn)換 
              __gc class A { 
              public: 
                 void Finalize() { Console::WriteLine(S"in ~A"); } 
              }; 
              __gc class B : public A { 
              public: 
                 void Finalize() {  
              Console::WriteLine(S"in ~B");   
              A::Finalize();  
                 } 
              }; 
              
            2. 第二步中,編譯器產(chǎn)生一個虛析構(gòu)函數(shù)。這個析構(gòu)函數(shù)就是我們的 V1用戶程序直接調(diào)用或者通過 delete表達式的應(yīng)用程序調(diào)用的。它永遠不會被垃圾回收器調(diào)用。

              這個產(chǎn)生的析構(gòu)函數(shù)里面有什么內(nèi)容呢?是兩個語句。一個是調(diào)用GC::SuppressFinalize()以確保沒有對 Finalize()方法的進一步調(diào)用。另一個是實際上的 Finalize()調(diào)用。回憶一下,這表達了用戶提供的這個類的析構(gòu)函數(shù)。如下所示:

              __gc class A { 
              public: 
                    virtual ~A()  
              { 
                       System::GC::SuppressFinalize(this); 
                       A::Finalize(); 
                    } 
              }; 
              __gc class B : public A { 
              public: 
                    virtual ~B()  
              {  
                          System::GC:SuppressFinalize(this); 
                       B::Finalize(); 
                    } 
              }; 
              

            這個實現(xiàn)允許用戶立刻顯式調(diào)用類的 Finalize()方法,而不是隨時調(diào)用,它并不真的依賴于使用 Dispose()方法的方案。這在修訂版語言設(shè)計中進行了更改。

            2.4.3 V2 中,析構(gòu)函數(shù)轉(zhuǎn)到 Dispose()

            在修訂版語言設(shè)計中,析構(gòu)函數(shù)被內(nèi)部重命名為 Dispose()方法,并且引用類自動擴展以實現(xiàn) IDisposable接口。換句話說,在 V2中,這對類按如下所示進行轉(zhuǎn)換:

            // V2 下析構(gòu)函數(shù)的內(nèi)部轉(zhuǎn)換 
            __gc class A : IDisposable { 
            public: 
               void Dispose() {  
               System::GC::SuppressFinalize(this); 
            Console::WriteLine( "in ~A"); } 
               } 
            }; 
            __gc class B : public A { 
            public: 
               void Dispose() {  
               System::GC::SuppressFinalize(this); 
            Console::WriteLine( "in ~B");   
            A::Dispose();  
               } 
            }; 
            

            在 V2 中,當析構(gòu)函數(shù)被顯式調(diào)用時,或者對跟蹤句柄應(yīng)用 delete時,底層的 Dispose()方法都會自動被調(diào)用。如果這是一個派生類,一個對基類的 Dispose()方法的調(diào)用會被插入到生成方法的末尾。

            但是這樣也沒有給我們確定性終止的方法。為了解決這個問題,我們需要局部引用對象的額外支持(在原版語言設(shè)計中沒有類似的支持,所以沒有轉(zhuǎn)換的問題)。

            2.4.4 聲明一個引用對象

            修訂版語言支持在本地棧上聲明引用類的對象,或者聲明為類的成員,就像它可以直接被訪問一樣(注意這在 Microsoft Visual Studio 2005 的Beta1 發(fā)布版中不可用)。析構(gòu)函數(shù)和在 2.4.3 節(jié)中描述的 Dispose() 方法結(jié)合時,結(jié)果就是引用類型的終止語義的自動調(diào)用。使 CLI 社區(qū)苦惱的非確定性終止這條暴龍終于被馴服了,至少對于 C++/CLI的用戶來說是這樣。讓我們看一下這到底意味著什么。

            首先,我們這樣定義一個引用類,使得對象創(chuàng)建函數(shù)在類構(gòu)造函數(shù)中獲取一個資源。其次,在類的析構(gòu)函數(shù)中,釋放對象創(chuàng)建時獲得的資源。

            public ref class R { 
            public: 
                R() { /* 獲得外部資源 */ } 
                  ~R(){ /* 釋放外部資源 */ } 
                  // ... 雜七雜八 ... 
            }; 
            

            對象聲明為局部的,使用沒有附加"帽子"的類型名。所有對對象的使用(如調(diào)用成員函數(shù))是通過成員選擇點 (.) 而不是箭頭 (->) 完成的。在塊的末尾,轉(zhuǎn)換成 Dispose()的相關(guān)的析構(gòu)函數(shù)被自動調(diào)用。

            void f() 
            { 
                R r;  
                r.methodCall(); 
                // ... 
                // r被自動析構(gòu) - 
                // 也就是說, r.Dispose() 被調(diào)用...  
            } 
            

            相對于 C#中的 using語句來說,這只是語法上的點綴而已,而不是對基本 CLI約定(所有引用類型必須在 CLI堆上分配)的違背?;A(chǔ)語法仍未變化。用戶可能已經(jīng)編寫了下面同樣功能的語句(這很像編譯器執(zhí)行的內(nèi)部轉(zhuǎn)換):

            // 等價的實現(xiàn)... 
            // 除了它應(yīng)該位于一個 try/finally 語句中之外 
            void f() 
            { 
                R^ r = gcnew R;  
                r->methodCall(); 
                // ... 
                delete r; 
            } 
            

            事實上,在修訂版語言設(shè)計中,析構(gòu)函數(shù)再次與構(gòu)造函數(shù)配對成為和一個局部對象生命周期關(guān)聯(lián)的自動獲得/釋放資源的機制。這個顯著的成就非常令人震驚,并且語言設(shè)計者應(yīng)該因此被大力贊揚。

            2.4.5 聲明一個顯式的 Finalize()-(!R)

            在修訂版語言設(shè)計中,如我們所見,構(gòu)造函數(shù)被合成為 Dispose()方法。這意味著在析構(gòu)函數(shù)沒有被顯式調(diào)用的情況下,垃圾回收器在終止過程中,不會像以前那樣為對象查找相關(guān)的 Finalize()方法。為了同時支持析構(gòu)函數(shù)和終止,修訂版語言引入了一個特殊的語法來提供一個終止器。舉例來說:

            public ref class R { 
               public: 
                  !R() { Console::WriteLine( "I am the R::finalizer()!" ); } 
               }; 
            

            ! 前綴表示引入類析構(gòu)函數(shù)的類似符號 (~),也就是說,兩種后生命周期的方法名都是在類名前加一個符號前綴。如果派生類中有一個合成的 Finalize()方法,那么在其末尾會插入一個基類的Finalize()方法的調(diào)用。如果析構(gòu)函數(shù)被顯式地調(diào)用,那么終止器會被抑制。這個轉(zhuǎn)換如下所示:

            // V2 中的內(nèi)部轉(zhuǎn)換 
            public ref class R { 
            public: 
            void Finalize() 
               { Console::WriteLine( "I am the R::finalizer()!" ); } 
            };  
            

            2.4.6 這在 V1  V2 的轉(zhuǎn)換中意味著什么

            這意味著,只要一個引用類包含一個特別的析構(gòu)函數(shù),一個 V1程序在 V2 編譯器下的運行時行為被靜默地修改了。需要的轉(zhuǎn)換算法如下所示:

            • 如果析構(gòu)函數(shù)存在,重寫它為類終止器方法。

            • 如果 Dispose()方法存在,重寫到類析構(gòu)函數(shù)中。

            • 如果析構(gòu)函數(shù)存在,但是 Dispose()方法不存在,保留析構(gòu)函數(shù)并且執(zhí)行第 (1) 項。

            在將代碼從 V1移植到 V2的過程中,可能漏掉執(zhí)行這個轉(zhuǎn)換。如果應(yīng)用程序某種程度上依賴于相關(guān)終止方法的執(zhí)行,那么應(yīng)用程序的行為將被靜默地修改。

            3.類或接口中的成員聲明

            屬性和操作符的聲明在修訂版語言設(shè)計中已經(jīng)被大范圍重寫了,隱藏了原版設(shè)計中暴露的底層實現(xiàn)細節(jié)。另外,事件聲明也被修改了。

            在 V1中不受支持的一項更改是,靜態(tài)構(gòu)造函數(shù)現(xiàn)在可以在類外部定義了(在 V1中它們必須被定義為內(nèi)聯(lián)的),并且引入了委托構(gòu)造函數(shù)的概念。

            3.1 屬性聲明

            在原版語言設(shè)計中,每一個 set或者 get屬性存取方法都被規(guī)定為一個獨立的成員函數(shù)。每個方法的聲明都由 __property關(guān)鍵字作為前綴。方法名以 set_或者 get_開頭,后面接屬性的實際名稱(如用戶所見)。這樣,一個獲得向量的 x坐標的屬性存取方法將命名為 get_x,用戶將以名稱 x來調(diào)用它。這個名稱約定和單獨的方法規(guī)定實際上反映了屬性的基本運行時實現(xiàn)。例如,以下是我們的向量,有一些坐標屬性:

            public __gc __sealed class Vector  
            { 
            public: 
               // ... 
               __property double get_x(){ return _x; } 
               __property double get_y(){ return _y; } 
               __property double get_z(){ return _z; } 
               __property void set_x( double newx ){ _x = newx; } 
               __property void set_y( double newy ){ _y = newy; } 
               __property void set_z( double newz ){ _z = newz; } 
            }; 
            

            這使人感到迷惑,因為屬性相關(guān)的函數(shù)被展開了,并且需要用戶從語法上統(tǒng)一相關(guān)的 set  get。而且它在語法上過于冗長,并且感覺上不甚優(yōu)雅。在修訂版語言設(shè)計中,這個聲明更類似于 C# — property 關(guān)鍵字后接屬性的類型以及屬性的原名。set 存取get 存取方法放在屬性名之后的一段中。注意,與 C# 不同,存取方法的符號被指出。例如,以下是上面的代碼轉(zhuǎn)換為新語言設(shè)計后的結(jié)果:

            public ref class Vector sealed 
            {  
            public: 
               property double x  
               { 
                  double get() 
                  { 
                     return _x; 
                  } 
                  void set( double newx ) 
                  { 
                     _x = newx; 
                  } 
               } // Note: no semi-colon ... 
            }; 
            

            如果屬性的存取方法表現(xiàn)為不同的訪問級別 — 例如一個公有的 get和一個私有的或者保護的 set,那么可以指定一個顯式的訪問標志。默認情況下,屬性的訪問級別反映了它的封閉訪問級別。例如,在上面的 Vector定義中,get set方法都是公有的。為了讓 set方法成為保護或者私有的,必須如下修改定義:

            public ref class Vector sealed 
            {  
            public: 
               property double x  
               { 
                  double get() 
                  { 
                     return _x; 
                  } 
               private: 
                  void set( double newx ) 
                  { 
                     _x = newx; 
                  } 
               } // 注意:private 的作用域到此結(jié)束 ... 
               //注意:dot 是一個 Vector 的公有方法... 
               double dot( const Vector^ wv ); 
            // etc. 
            }; 
            

            屬性中訪問關(guān)鍵字的作用域延伸到屬性的結(jié)束括號或者另一個訪問關(guān)鍵字的說明。它不會延伸到屬性的定義之外,直到進行屬性定義的封閉訪問級別。例如,在上面的聲明中,Vector::dot()是一個公有成員函數(shù)。

            為三個 Vector坐標編寫 set/get屬性有點乏味,因為實現(xiàn)的本質(zhì)是定死的:(a) 用適當類型聲明一個私有狀態(tài)成員,(b) 在用戶希望取得其值的時候返回,以及 (c) 將其設(shè)置為用戶希望賦予的任何新值。在修訂版語言設(shè)計中,一個簡潔的屬性語法可以用于自動化這個使用方式:

            public ref class Vector sealed 
            {  
            public: 
               //等價的簡潔屬性語法 
               property double x;  
            property double y; 
            property double z; 
            }; 
            

            簡潔屬性語法所產(chǎn)生的一個有趣的現(xiàn)象是,在編譯器自動生成后臺狀態(tài)成員時,除非通過 set/get訪問函數(shù),否則這個成員在類的內(nèi)部不可訪問。這就是所謂的嚴格限制的數(shù)據(jù)隱藏!

            3.2 屬性索引聲明

            原版語言對索引屬性的支持的兩大缺點是不能提供類級別的下標,也就是說,所有索引屬性必須有一個名字,舉例來說,這樣就沒有辦法提供可以直接應(yīng)用到一個 Vector或者Matrix類對象的托管下標操作符。其次,一個次要的缺點是很難在視覺上區(qū)分屬性和索引屬性 — 參數(shù)的數(shù)目是唯一的判斷方法。最后,索引屬性具有與非索引屬性同樣的問題 — 存取函數(shù)沒有作為一個基本單位,而是分為單獨的方法。舉例來說:

            public __gc class Vector; 
            public __gc class Matrix 
            { 
                float mat[,]; 
            public:  
               __property void set_Item( int r, int c, float value); 
               __property int get_Item( int r, int c ); 
               __property void set_Row( int r, Vector* value ); 
               __property int get_Row( int r ); 
            }; 
            

            如您所見,只能用額外的參數(shù)來指定一個二維或者一維的索引,從而區(qū)分索引器。在修訂版語法中,索引器由名字后面的方括號 ([,]) 區(qū)分,并且表示每個索引的數(shù)目和類型:

            public ref class Vector; 
            public ref class Matrix 
            { 
            private: 
               array<float, 2>^ mat; 
            public: 
               property int Item [int,int] 
               { 
                  int get( int r, int c ); 
                  void set( int r, int c, float value ); 
               } 
               property int Row [int] 
               { 
                  int get( int r ); 
                  void set( int r, Vector^ value ); 
               } 
            }; 
            

            在修訂版語法中,為了指定一個可以直接應(yīng)用于類對象的類級別索引器,重用 default關(guān)鍵字以替換一個顯式的名稱。例如:

            public ref class Matrix
            {
            private:
               array<float, 2>^ mat;
            public:
                  //OK,現(xiàn)在有類級別的索引器了
                   //
                   // Matrix mat ...
                   //     mat[ 0, 0 ] = 1; 
                   //
                   // 調(diào)用默認索引器的 set 存取函數(shù)...
               property int default [int,int]
               {
                   int get( int r, int c );
                   void set( int r, int c, float value );
               }
               property int Row [int]
               {
                  int get( int r );
                  void set( int r, Vector^ value );
               }
            };
            

            在修訂版語法中,當指定了 default索引屬性時,下面兩個名字被保留:get_Itemset_Item。這是因為它們是 default索引屬性產(chǎn)生的底層名稱。

            注意,簡單索引語法與簡單屬性語法截然不同。

            3.3 委托和事件

            聲明一個委托和普通事件僅有的變化是移除了雙下劃線,如下面的示例所述。在去掉了之后,這個更改被認為是完全沒有爭議的。換句話說,沒有人支持保持雙下劃線,所有人現(xiàn)在看來都同意雙下劃線使得原版語言感覺很難看。

            // 原版語言 (V1)  
            __delegate void ClickEventHandler(int, double); 
            __delegate void DblClickEventHandler(String*); 
            __gc class EventSource { 
                     __event ClickEventHandler* OnClick;   
                     __event DblClickEventHandler* OnDblClick;   
                  // ... 
            }; 
            // 修訂版語言 (V2)  
            delegate void ClickEventHandler( int, double ); 
            delegate void DblClickEventHandler( String^ ); 
            ref class EventSource 
            { 
               event ClickEventHandler^ OnClick;  
               event DblClickEventHandler^ OnDblClick;  
            // ... 
            }; 
            

            事件(以及委托)是引用類型,這在 V2中更為明顯,因為有帽子 (^) 的存在。除了普通形式之外,事件支持一個顯式的聲明語法,用戶顯式指定事件關(guān)聯(lián)的 add()raise()、和 remove()方法。(只有 add()和 remove()方法是必須的;raise()方法是可選的)。

            在 V1設(shè)計中,如果用戶選擇提供這些方法,盡管她必須決定尚未存在的事件的名稱,她也不必提供一個顯式的事件聲明。每個單獨的方法以 add_EventName、raise_EventName、和remove_EventName的格式指定,如以下引用自 V1語言規(guī)范的示例所述:

            // 原版 V1 語言下 
            // 顯式地實現(xiàn) add、remove 和 raise ... 
            public __delegate void f(int); 
            public __gc struct E { 
               f* _E; 
            public: 
               E() { _E = 0; } 
               __event void add_E1(f* d) { _E += d; } 
               static void Go() { 
                  E* pE = new E; 
                  pE->E1 += new f(pE, &E::handler); 
                  pE->E1(17);  
                  pE->E1 -= new f(pE, &E::handler); 
                  pE->E1(17);  
               } 
            private: 
               __event void raise_E1(int i) { 
                  if (_E) 
                     _E(i); 
               } 
            protected: 
               __event void remove_E1(f* d) { 
                  _E -= d; 
               } 
            }; 
            

            該設(shè)計的問題主要是感官上的,而不是功能上的。雖然設(shè)計支持添加這些方法,但是上面的示例看起來并不是一目了然。因為 V1屬性和索引屬性的存在,類聲明中的方法看起來千瘡百孔。更令人沮喪的是缺少一個實際的 E1事件聲明。(再強調(diào)一遍,底層實現(xiàn)細節(jié)暴露了功能的用戶級別語法,這顯然增加了語法的復雜性。)這只是勞而無功。V2設(shè)計大大簡化了這個聲明,如下面的轉(zhuǎn)換所示。事件在事件聲明及其相關(guān)委托類型之后的一對花括號中指定兩個或者三個方法如下所示:

            // 修訂版 V2 語言設(shè)計 
            delegate void f( int ); 
            public ref struct E { 
            private: 
               f^ _E; //是的,委托也是引用類型 
            public: 
               E() 
               {  // 注意 0 換成了 nullptr! 
                  _E = nullptr;  
               } 
               // V2 中顯式事件聲明的語法聚合 
               event f^ E1 
               { 
               public: 
                  void add( f^ d ) 
                  { 
                     _E += d; 
                  } 
               protected: 
                  void remove( f^ d ) 
                  { 
                     _E -= d; 
                  } 
               private: 
                  void raise( int i ) 
                  { 
                     if ( _E ) 
                          _E( i ); 
                  } 
               } 
               static void Go() 
               { 
                  E^ pE = gcnew E; 
                  pE->E1 += gcnew f( pE, &E::handler ); 
                  pE->E1( 17 );  
                  pE->E1 -= gcnew f( pE, &E::handler ); 
                  pE->E1( 17 );  
               } 
            }; 
            

            雖然在語言設(shè)計方面,人們因為語法的簡單枯燥而傾向于忽視它,但是如果對語言的用戶體驗有很大的潛移默化的影響,那么它實際上很有意義。一個令人迷惑的、不優(yōu)雅的語法可能增加開發(fā)過程的風險,很大程度上就像一個臟的或者不清晰的擋風玻璃增加開車的風險一樣。在修訂版語言設(shè)計中,我們努力使語法像一塊高度磨光的新安裝的擋風玻璃一樣透明。

            3.4 密封一個虛函數(shù)

            __sealed關(guān)鍵字在 V1版中用于修飾一個引用類型,禁止從此繼續(xù)派生 — 如 2.1.2 節(jié)所述 — 或者修飾一個虛函數(shù),禁止從派生類中繼續(xù)重寫方法。舉例來說:

            class base { public: virtual void f(); }; 
            class derived : public base { 
            public: 
               __sealed void f(); 
            }; 
            

            在此示例中,derived::f()根據(jù)函數(shù)原型的完全匹配來重寫 base::f()實例。__sealed關(guān)鍵字指明一個繼承自 derived類的后續(xù)類不能重寫 derived::f()。

            在新的語言設(shè)計中,sealed放在符號之后,而不是像在 V1 中那樣,允許放在實際函數(shù)原型之前任何位置。另外,sealed的使用也需要顯式使用 virtual關(guān)鍵字。換句話說,上面的 derived的正確轉(zhuǎn)換如下所述:

            class derived: public base 
            { 
            public: 
               virtual void f() sealed; 
            }; 
            

            缺少 virtual關(guān)鍵字會產(chǎn)生一個錯誤。在 V2中,上下文關(guān)鍵字 abstract可以在 =0 處用來指明一個純虛函數(shù)。這在 V1中不被支持。舉例來說:

            class base { public: virtual void f()=0; }; 
            

            可以改寫為

            class base { public: virtual void f() abstract; }; 
            

            3.5 操作符重載

            原版語言設(shè)計最驚人之處可能是它對于操作符重載的支持 — 或者更恰當?shù)卣f,是有效的缺乏支持。舉例來說,在一個引用類型的聲明中,不是使用內(nèi)建的 operator+語法,而是必須顯式編寫出操作符的底層內(nèi)部名稱 — 在本例中是 op_Addition。但更加麻煩的是,操作符的調(diào)用必須通過該名稱來顯式觸發(fā),這樣就妨礙了操作符重載的兩個主要好處:(a) 直觀的語法,和 (b) 混合現(xiàn)有類型和新類型的能力。舉例來說:

            public __gc __sealed class Vector { 
            public: 
              Vector( double x, double y, double z ); 
              static bool    op_Equality( const Vector*, const Vector* ); 
              static Vector* op_Division( const Vector*, double ); 
              static Vector* op_Addition( const Vector*, const Vector* ); 
              static Vector* op_Subtraction( const Vector*, const Vector* ); 
            }; 
            int main() 
            { 
              Vector *pa = new Vector( 0.231, 2.4745, 0.023 ); 
              Vector *pb = new Vector( 1.475, 4.8916, -1.23 );  
              Vector *pc1 = Vector::op_Addition( pa, pb ); 
              Vector *pc2 = Vector::op_Subtraction( pa, pc1 ); 
              Vector *pc3 = Vector::op_Division( pc1, pc2->x() ); 
              if ( Vector::op_Equality( pc1, p2 ))  
                // ... 
            } 
            

            在語言的修訂版中,滿足了傳統(tǒng) C++程序員的普通期望,聲明和使用靜態(tài)操作符。以下是轉(zhuǎn)換為 V2語法的 Vector類:

            public ref class Vector sealed { 
            public: 
               Vector( double x, double y, double z ); 
               static bool    operator ==( const Vector^, const Vector^ ); 
               static Vector^ operator /( const Vector^, double ); 
               static Vector^ operator +( const Vector^, const Vector^ ); 
               static Vector^ operator -( const Vector^, const Vector^ ); 
            }; 
            int main() 
            { 
               Vector^ pa = gcnew Vector( 0.231, 2.4745, 0.023 ), 
               Vector^ pb = gcnew Vector( 1.475,4.8916,-1.23 ); 
               Vector^ pc1 = pa + pb; 
               Vector^ pc2 = pa-pc1; 
               Vector^ pc3 = pc1 / pc2->x(); 
               if ( pc1 == p2 ) 
                    // ... 
            } 
            

            3.6 轉(zhuǎn)換操作符

            談到令人不愉快的感覺,在 V1語言設(shè)計中必須編寫 op_Implicit來指定一個轉(zhuǎn)換感覺上就不像 C++。例如,以下是引自 V1語言規(guī)范的 MyDouble定義:

            __gc struct MyDouble  
            { 
               static MyDouble* op_Implicit( int i );  
               static int op_Explicit( MyDouble* val ); 
               static String* op_Explicit( MyDouble* val );  
            }; 
            

            這就是說,給定一個整數(shù),將這個整數(shù)轉(zhuǎn)換為 MyDouble的算法是通過op_Implicit操作符實現(xiàn)的。進一步說,這個轉(zhuǎn)換將被編譯器隱式執(zhí)行。類似的,給定一個 MyDouble對象,兩個op_Explicit操作符分別提供了以下兩種算法:將對象轉(zhuǎn)換為整型或者托管字符串實體。但是,編譯器不會執(zhí)行這個轉(zhuǎn)換,除非用戶顯式要求。

            在 C#中,如下所示:

            class MyDouble  
            { 
               public static implicit operator MyDouble( int i );  
               public static explicit operator int( MyDouble val ); 
               public static explicit operator string( MyDouble val );  
            }; 
            

            除了每個成員都有的顯式公有 訪問標志看起來很古怪,C#代碼看起來比 C++的托管擴展更加像 C++。所以我們不得不修復這個問題。但是我們怎么才能做到?

            一方面,C++程序員將構(gòu)建為轉(zhuǎn)換操作符單參數(shù)構(gòu)造函數(shù)省略掉。但是,另一方面,該設(shè)計被證明是如此難于處理,以致于 ISO-C++委員會引入了一個關(guān)鍵字 explicit,只是為了處理它的意外后果— 例如,有一個整型參數(shù)作為維數(shù)的 Array類隱式地將任何整型變量轉(zhuǎn)換為 Array對象,甚至在用戶最不需要時也這樣。Andy Koenig 是第一個引起我注意的人,他解釋了一個設(shè)計習慣,構(gòu)造函數(shù)中的第二虛參數(shù)只是用來阻止這種不好的事情的發(fā)生。所以我不會對 C++/CLI中缺乏單構(gòu)造函數(shù)隱式轉(zhuǎn)換而感到遺憾。

            另一方面,在 C++中設(shè)計一個類類型時提供一個轉(zhuǎn)換對從來不是一個好主意。這方面最好的示例是標準 string類。隱式轉(zhuǎn)換是有一個 C風格字符串的單參數(shù)構(gòu)造函數(shù)。但是,它沒有提供一個對應(yīng)的隱式轉(zhuǎn)換操作符來將 string 對象轉(zhuǎn)換為 C風格的字符串 — 而是需要用戶顯式調(diào)用一個命名函數(shù) — 在這個示例中是 c_str()。

            這樣,將轉(zhuǎn)換操作符的隱式/顯式行為進行關(guān)聯(lián)(以及將一組轉(zhuǎn)換封裝到一組聲明)看起來是原始 C++ 對轉(zhuǎn)換操作符支持的改進,這個支持自從 1988 年 Robert Murray 發(fā)布了關(guān)于 UsenixC++的標題為 Building Well-Behaved Type Relationships in C++的講話之后,已經(jīng)成為一個公開的警世篇,講話最終產(chǎn)生了 explicit 關(guān)鍵字。修訂版 V2語言對轉(zhuǎn)換操作符的支持如下所示,比 C# 的支持稍微簡略一點,因為操作符的默認行為支持隱式轉(zhuǎn)換算法的應(yīng)用:

            ref struct MyDouble 
            { 
            public: 
               static operator MyDouble^ ( int i ); 
               static explicit operator int ( MyDouble^ val ); 
               static explicit operator String^ ( MyDouble^ val ); 
            }; 
            

            V1到 V2的另一個變化是,V2中的單參數(shù)構(gòu)造函數(shù)以聲明為 explicit 的方式處理。這意味著為了觸發(fā)它的調(diào)用,需要一個顯式的轉(zhuǎn)換。但是要注意,如果一個顯式的轉(zhuǎn)換操作符已經(jīng)定義,那么是它而不是單參數(shù)構(gòu)造函數(shù)會被調(diào)用。

            3.7 接口成員的顯式重寫

            經(jīng)常有必要在實現(xiàn)接口的類中提供兩個接口成員的實例 — 一個用于通過接口句柄操作類對象,另一個用于通過類界面使用對象。例如:

            public __gc class R : public ICloneable  
            { 
               // 通過ICloneable使用... 
               Object* ICloneable::Clone(); 
               // 通過一個R對象使用 ... 
               R* Clone(); 
            }; 
            

            在 V1中,我們通過一個用接口名限定的方法名來提供接口方法的顯式聲明,從而解決這個問題。特定于類的實例是未被限定的。在這個示例中,當通過 R的一個實例顯式調(diào)用 Clone()時,這樣可以免除對其返回值的類型向下強制轉(zhuǎn)換。

            在 V2中,一個通用重寫機制被引入,用來替換前面的語法。我們的示例會被重寫,如下所示:

            public ref class R : public ICloneable  
            { 
               // 通過 ICloneable 使用 ... 
               Object^ InterfaceClone() = ICloneable::Clone; 
               // 通過一個 R 對象使用 ... 
               virtual R^ Clone() new; 
            }; 
            

            這個修訂要求為顯式重寫的接口成員賦予一個在類中唯一的名稱。這里我提供了一個有些笨拙的名稱 InterfaceClone()。修訂版的行為仍舊是相同的 — 通過 ICloneable接口的調(diào)用觸發(fā)重命名的InterfaceClone(),而通過 R 類型對象的調(diào)用調(diào)用第二個 Clone()實例。

            3.8 私有虛函數(shù)

            在 V1中,虛函數(shù)的訪問級別并不影響它在派生類中是否可以被重寫。這在 V2中被修改了。在 V2中,虛函數(shù)不能重寫不可訪問的基類虛函數(shù)。例如:

            __gc class My{ //在派生類中無法訪問...virtual void g();};__gc class File : public My {public: // 正確:在 V 1中,g() 重寫了 My::g() // 錯誤:在 V2 中,不能重寫: My::g() 無法訪問...void g();};

            對于這種設(shè)計而言,實際上沒有在 V2中的對應(yīng)。要重寫這個函數(shù),必須使基類的成員可訪問 — 也就是說,非私有的。繼承的方法不必沿用同樣的訪問級別。在這個示例中,最小的改變是將 My成員聲明為保護的。這樣,一般的程序通過 My來訪問這個方法仍舊是被禁止的。

            ref class My { 
            protected: 
            virtual void g(); 
            }; 
            ref class File : My { 
            public: 
            void g(); 
            }; 
            

            注意在 V2 下,如果基類缺少顯式的 virtual關(guān)鍵字,那么會產(chǎn)生一個警告消息。

            3.9 靜態(tài)常量整型的連接方式 (linkage) 不再是 literal 的

            雖然 static const整型成員仍舊被支持,但是它們的 linkage 屬性被修改了。以前的 linkage 屬性現(xiàn)在通過一個 literal整型成員來完成。例如,考慮如下 V1類:

            public __gc class Constants { 
            public: 
            static const int LOG_DEBUG = 4; 
            // ... 
            }; 
            

            它為域產(chǎn)生如下的底層 CIL屬性(注意黑體的 literal 屬性):

            .field public static literal int32  
            modopt([Microsoft.VisualC]Microsoft.VisualC.IsConstModifier) STANDARD_CLIENT_PRX = int32(0x00000004) 
            

            它雖然在 V2 語法下仍舊可以編譯,

            public ref class Constants { 
            public: 
            static const int LOG_DEBUG = 4; 
            // ... 
            }; 
            

            但是不再產(chǎn)生 literal屬性,所以不被 CLI運行庫視為一個常量。

            .field public static int32 modopt([Microsoft.VisualC]Microsoft.VisualC.IsConstModifier) 
            STANDARD_CLIENT_PRX = int32(0x00000004)
            

            為了具有同樣的中間語言的 literal屬性,聲明應(yīng)該改為使用新支持的 literal數(shù)據(jù)成員,如下所示:

            public ref class Constants { 
            public: 
            literal int LOG_DEBUG = 4; 
            // ... 
            }; 
            

            4 值類型及其行為

            本節(jié)中我們著眼于 CLI枚舉類型和值類類型,同時研究裝箱和對 CLI堆上裝箱實例的訪問,以及考慮內(nèi)部和釘住指針。這個領(lǐng)域的語言變化范圍很廣。

            4.1 CLI 枚舉類型

            原版語言的 CLI枚舉聲明前有一個 __value 關(guān)鍵字。 這里的意圖是區(qū)分本機枚舉和派生自 System::ValueType  CLI枚舉,同時暗示它們具有同樣的功能。例如,

            __value enum e1 { fail, pass }; 
            public __value enum e2 : unsigned short  {  
            not_ok = 1024,  
            maybe, ok = 2048  
            };   
            

            修訂版語言用強調(diào)后者的類本質(zhì)而不是其值類型本源的方法來解決這個區(qū)分本機枚舉和 CLI枚舉的問題。同樣,__value關(guān)鍵字被廢棄了,替換成了一對由空格分隔的關(guān)鍵字 enum class。這實現(xiàn)了引用類、值類接口類聲明中關(guān)鍵字對的對稱。

            enum class ec; 
            value class vc; 
            ref class rc; 
            interface class ic; 
            

            修訂版語言設(shè)計中的枚舉對 e1  e2的轉(zhuǎn)換如下所示:

            enum class e1 { fail, pass }; 
            public enum class e2 : unsigned short {  
            not_ok = 1024, 
            maybe, ok = 2048  
            }; 
            

            除了這種句法上的小小修改之外,托管枚舉類型的行為在很多方面有所改變:

            1. CLI枚舉的前置聲明在 V2中不再支持。V2 中沒有這樣的對應(yīng)。這只會導致編譯時錯誤。

              __value enum status; // V1: 正確 
              enum class status;   // V2: 錯誤 
              
            2. 在內(nèi)建算術(shù)類型和對象類層次結(jié)構(gòu)之間進行重載解析的次序在 V2和 V1中被顛倒了!一個副作用是,托管枚舉在 V2 中不能再像在 V1中一樣隱式轉(zhuǎn)換成算術(shù)類型。

            3. 與在 V1 中不同,在 V2中,托管枚舉具有它自己的范圍。在 V1中,枚舉數(shù)在包含枚舉的范圍內(nèi)可見。在 V2中,枚舉數(shù)被限定在枚舉的范圍內(nèi)。

            4.1.1 CLI 枚舉是一種類型

            舉例來說,考慮如下代碼片斷:

            __value enum status { fail, pass }; 
            void f( Object* ){ cout << "f(Object)\n"; } 
            void f( int ){ cout << "f(int)\n"; } 
            int main() 
            { 
               status rslt; 
               // ... 
               f( rslt ); // which f is invoked? 
            } 
            

            對于本機 C++程序員來說,該問題自然的答案是,被調(diào)用的重載 f()的實例是 f(int)。枚舉是一個整型符號常量,并且在此示例中作為標準整型被轉(zhuǎn)換。實際上,在原版語言設(shè)計中,這事實上就是調(diào)用解析的實例。這產(chǎn)生了一些意想不到的結(jié)果 — 不是在我們以本機 C++框架思想使用它的時候 — 而是在我們需要它們與現(xiàn)存的 BCL(基類庫)框架交互的時候,這里枚舉是一個間接派生自 Object的類。在修訂版語言設(shè)計中,被調(diào)用的 f()實例是 f(Object^)。

            V2選擇強制不支持 CLI枚舉算術(shù)類型之間的隱式轉(zhuǎn)換。這意味著任何從托管枚舉類型對象算術(shù)類型的賦值都需要一個顯式的強制轉(zhuǎn)換。舉例來說,假定

               void f( int ); 
            

            是一個非重載方法,在 V1中,調(diào)用

               f( rslt ); // ok: V1; error: V2 
            

            是可行的,rslt 中的值被隱式轉(zhuǎn)換為一個整型值。在 V2中,這個調(diào)用的編譯會失敗。要正確轉(zhuǎn)換它,我們必須插入一個轉(zhuǎn)換操作符:

               f( safe_cast<int>( rslt )); // ok: V2 
            

            4.1.2 CLI 枚舉類型的范圍

            C和 C++語言之間的不同之一就是 C++在 struct 中添加了范圍。在 C中,struct 只是一個數(shù)據(jù)的聚合,既不支持接口也不支持關(guān)聯(lián)的范圍。這在當時是一個十分激進的改變,并且對于很多從 C 語言轉(zhuǎn)移過來的新 C++ 用戶來說是一個有爭議的問題。本機和 CLI 的枚舉的關(guān)系也類似。

            在原始語言設(shè)計中,曾經(jīng)嘗試過為托管枚舉的枚舉數(shù)定義弱插入名稱,用于模擬本機枚舉內(nèi)范圍的缺失。這個嘗試被證明是失敗的,問題在于這造成了枚舉數(shù)溢出到全局命名空間,造成了管理名稱沖突的困難。在修訂版語言中,我們按照其他 CLI語言來支持托管枚舉的范圍。

            這意味著 CLI 枚舉的枚舉數(shù)的任何未限定使用將不能被修訂版語言識別。讓我們來看一個實際的例子。

            // 原版語言設(shè)計支持弱插入 
            __gc class XDCMake { 
            public: 
              __value enum _recognizerEnum {  
                 UNDEFINED, 
                 OPTION_USAGE,  
                 XDC0001_ERR_PATH_DOES_NOT_EXIST = 1, 
                 XDC0002_ERR_CANNOT_WRITE_TO = 2, 
                 XDC0003_ERR_INCLUDE_TAGS_NOT_SUPPORTED = 3, 
                 XDC0004_WRN_XML_LOAD_FAILURE = 4, 
                 XDC0006_WRN_NONEXISTENT_FILES = 6, 
              }; 
              ListDictionary* optionList; 
              ListDictionary* itagList; 
              XDCMake()  
              { 
                 optionList = new ListDictionary; 
                 // here are the problems ... 
                 optionList->Add(S"?", __box(OPTION_USAGE));   // (1) 
                 optionList->Add(S"help", __box(OPTION_USAGE));   // (2) 
                 itagList = new ListDictionary; 
                 itagList->Add(S"returns",            
                               __box(XDC0004_WRN_XML_LOAD_FAILURE)); // (3) 
               } 
            }; 
            

            三個枚舉數(shù)名稱的未限定使用 ((1)、(2)(3)) 都需要在轉(zhuǎn)換為修訂版語言語法時被限定,從而讓源代碼通過編譯。以下是原始源代碼的正確轉(zhuǎn)換:

            ref class XDCMake 
            { 
            public: 
              enum class _recognizerEnum 
              { 
                 UNDEFINED, OPTION_USAGE,  
                 XDC0001_ERR_PATH_DOES_NOT_EXIST = 1, 
                 XDC0002_ERR_CANNOT_WRITE_TO = 2, 
                 XDC0003_ERR_INCLUDE_TAGS_NOT_SUPPORTED = 3, 
                 XDC0004_WRN_XML_LOAD_FAILURE = 4, 
                 XDC0006_WRN_NONEXISTENT_FILES = 6 
              }; 
              ListDictionary^ optionList; 
              ListDictionary^ itagList; 
              XDCMake() 
              { 
                optionList = gcnew ListDictionary; 
                optionList->Add("?",_recognizerEnum::OPTION_USAGE); // (1) 
                optionList->Add("help",_recognizerEnum::OPTION_USAGE); //(2) 
                itagList = gcnew ListDictionary; 
                itagList->Add( "returns",  
                         recognizerEnum::XDC0004_WRN_XML_LOAD_FAILURE); //(3) 
              } 
            }; 
            

            這改變了本機和 CLI 枚舉之間的設(shè)計策略。因為 CLI 枚舉在 V2中保持一個關(guān)聯(lián)的范圍,在一個類中封裝枚舉的聲明不再是有必要和有效的了。這個用法隨著貝爾實驗室的 cfront 2.0而不斷發(fā)展,也用來解決全局名稱污染的問題。

            在貝爾實驗室的 Jerry Schwarz 所創(chuàng)建的 beta 原版新 iostream庫中,Jerry 沒有封裝庫中定義的全部相關(guān)枚舉,而且通用枚舉數(shù) — 例如 read、write、append等 — 使得用戶幾乎不可能編譯他們的現(xiàn)存代碼。一個解決方案是破壞這些名稱,例如 io_read  io_write等等。另一個解決方案是修改語言來添加枚舉的范圍,但是在當時是不可能實現(xiàn)的。(一個折衷的方案是將枚舉封裝在類或類層次結(jié)構(gòu)中,這時枚舉的標記名稱及其枚舉數(shù)填充封閉類范圍。)換句話說,將枚舉放在類中的動機 — 至少是原始動機 — 不是理論上的,而是全局命名空間污染問題的一個實際解決方案。

            對于 V2CLI 枚舉,將枚舉封裝在類中不再有任何明顯的好處。實際上,如果您看看 System命名空間,您就會看到枚舉、類和接口都在同一個聲明空間中存在。

            4.2 隱式裝箱

            OK,我們食言了。在政治領(lǐng)域中,這會使我們輸?shù)粢粓鲞x舉。在語言設(shè)計中,這意味著我們在實際經(jīng)驗中強加了一個理論的位置,而且實際上它是一個錯誤。一個類似的情形是,在原始多繼承語言設(shè)計中,Stroustrup 認為在派生類的構(gòu)造函數(shù)中無法初始化一個虛基類子對象,這樣,C++ 語言要求任何作為虛基類的類都必須定義一個默認構(gòu)造函數(shù)。這樣只有默認的構(gòu)造函數(shù)才會被后續(xù)的虛派生調(diào)用。

            虛基類層次結(jié)構(gòu)的問題是將初始化共享虛子對象的職責轉(zhuǎn)推到每個后續(xù)的派生類中。舉例來說,我定義了一個基類,它的初始化需要分配一個緩沖區(qū),用戶指定的緩沖區(qū)大小作為構(gòu)造函數(shù)的一個參數(shù)傳遞。如果我提供了兩個后續(xù)的虛繼承,名為 inputb outputb,每個都需要提供基類構(gòu)造函數(shù)的一個特定值?,F(xiàn)在我從 inputb outputb派生一個 in_out類,那么兩個共享虛基類子對象的值都沒有明顯地被求值。

            因此,在原版語言設(shè)計中,Stroustrup 在派生類構(gòu)造函數(shù)的成員初始化列表中,禁用了虛基類的顯式初始化。雖然這解決了問題,但是實際上無法控制虛基類的初始化證明是不可行的。國家健康協(xié)會的 Keith Gorlen(他實現(xiàn)了一個名為 nihcl的免費版本 SmallTalk集合庫)勸告 Bjarne,讓他必須考慮一個更加靈活的語言設(shè)計。

            一個面向?qū)ο蟮膶哟卧O(shè)計原則是一個派生類只應(yīng)該涉及其本身和直接基類的非私有成員。為了支持一個靈活的虛繼承初始化設(shè)計,Bjarne 不得不破壞了這個原則。層次中最底層的類負責初始化所有虛子對象,不管他們在層次結(jié)構(gòu)中有多深。例如,inputb outputb都有責任顯式初始化他們的直虛基類。在從 inputb outputb派生 in_out類時,in_out開始負責初始化一度被移除的虛基類,并且 inputb outputb中的顯式初始化被抑制了。

            這提供了語言開發(fā)人員所需要的靈活性,但是卻以復雜的語義為代價。如果我們將虛基類限定為無狀態(tài),并且只允許指定一個接口,那么就消除了這種復雜性。這在 C++中是一個推薦的設(shè)計方案。在 C++/CLI中,這是 Interface類型的方針。

            以下是一個代碼實例,完成一些簡單的功能 — 在本例中,顯式裝箱很大程度上是無用的語法負擔。

               // 原版語言設(shè)計需要顯式 __box 操作 
            int my1DIntArray __gc[] = { 1, 2, 3, 4, 5 }; 
               Object* myObjArray __gc[] = {  
                         __box(26), __box(27), __box(28), __box(29), __box(30) 
                  }; 
               Console::WriteLine( "{0}\t{1}\t{2}", __box(0), 
                          __box(my1DIntArray->GetLowerBound(0)), 
                          __box(my1DIntArray->GetUpperBound(0)) ); 
            

            您可以了解,后面會有許多裝箱操作。在 V2中,值類型的裝箱是隱式的:

               // 修訂版語言進行隱式裝箱 
            array<int>^ my1DIntArray = {1,2,3,4,5}; 
            array<Object^>^ myObjArray = {26,27,28,29,30}; 
               Console::WriteLine( "{0}\t{1}\t{2}", 0,  
               my1DIntArray->GetLowerBound( 0 ),  
               my1DIntArray->GetUpperBound( 0 ) ); 
            

            4.3 裝箱值的跟蹤句柄

            裝箱是 CLI 統(tǒng)一類型系統(tǒng)的一個特性。值類型直接包含其狀態(tài),而引用類型有雙重含義:命名實體是一個句柄,這個句柄指向托管堆上分配的一個非命名對象。舉例來說,任何從類型到對象的初始化或者賦值,都需要類型放在 CLI 堆中(圖像裝箱發(fā)生的位置)首先分配相關(guān)的內(nèi)存,然后復制類型的狀態(tài),最后返回這個匿名 / 引用的組合。因此,用 C# 編寫如下代碼時,

            object o = 1024; // C# 隱式裝箱 
            

            代碼的簡潔使得裝箱十分接近透明。C# 的設(shè)計不僅隱藏了后臺所發(fā)生的操作的復雜性,而且也隱藏了裝箱本身的抽象性。另一方面,V1考慮到它可能導致效率降低,所以直接要求用戶顯式編寫指令:

            Object *o = __box( 1024 ); // V1 顯式裝箱  
            

            就像在本例中還有其他選擇一樣。依我之見,在這種情況下強迫用戶進行顯式請求就像一個人的老媽在他出門時不斷嘮叨一樣?,F(xiàn)在我們會照顧自己了,難道你不會?一方面,基于某些原因,一個人應(yīng)該學會內(nèi)斂,這被稱為成熟。另一方面,基于某些原因,一個人必須信任子女的成熟。把老媽換成語言的設(shè)計者,程序員換成子女,這就是 V2中裝箱成為隱式的原因。

            Object ^o = 1024; // V2 隱式裝箱 
            

            __box關(guān)鍵字在原版語言設(shè)計中是第二重要的服務(wù),這種設(shè)計在C#和 Microsoft Visual Basic .NET 語言中是沒有的:它提供詞匯表和跟蹤句柄來直接操作一個托管堆上的裝箱實例。例如,考慮如下小程序:

            int main() 
            { 
            double result = 3.14159; 
            __box double * by = __box( result ); 
            result = 2.7;  
            *br = 2.17;    
            Object * o = br; 
            Console::WriteLine( S"result :: {0}", result.ToString() ) ; 
            Console::WriteLine( S"result :: {0}", __box(result) ) ; 
            Console::WriteLine( S"result :: {0}", br ); 
            } 
            

            WriteLine的三個調(diào)用生成的底層代碼顯示了訪問裝箱值類型值的不同代價(感謝Yves Dolce指出這些差異),這里黑體的行顯示了與每個調(diào)用相關(guān)的開銷。

            // Console::WriteLine( S"result :: {0}", result.ToString() ) ; 
            ldstr      "result :: {0}" 
            ldloca.s   result 
            call       instance string  [mscorlib]System.Double::ToString() 
            call       void [mscorlib]System.Console::WriteLine(string, object) 
            // Console::WriteLine( S"result :: {0}", __box(result) ) ; 
            ldstr    " result :: {0}" 
            ldloc.0 
            box     [mscorlib]System.Double 
            call    void [mscorlib]System.Console::WriteLine(string, object) 
            // Console::WriteLine( S"result :: {0}", br ); 
            ldstr    "result :: {0}" 
            ldloc.0 
            call     void [mscorlib]System.Console::WriteLine(string, object) 
            

            直接將裝箱類型傳遞到 Console::WriteLin避免了裝箱和調(diào)用 ToString()的需要(當然,這是用前面提到的對 result的裝箱來初始化 br),所以除非真正使用 br,否則我們不會真正有所收獲。

            在修訂版語言語法中,在保持裝箱類型的優(yōu)點的同時,對它的支持也變得更加優(yōu)雅,并且集成到類型系統(tǒng)中。例如,以下是上面的小程序的轉(zhuǎn)換:

            int main() 
            { 
               double result = 3.14159; 
               double^ br = result; 
               result = 2.7; 
               *br = 2.17; 
               Object^ o = br; 
               Console::WriteLine( S"result :: {0}", result.ToString() ); 
               Console::WriteLine( S"result :: {0}", result ); 
               Console::WriteLine( S"result :: {0}", br ); 
            } 
            

            4.4 值類型語義

            以下是 V1語言規(guī)范中使用的一個規(guī)范的普通類型:

                        __value struct V { int i; }; 
                   __gc struct R { V vr; }; 
            

            在 V1中,我們可以有 4 種類型的語法變種(這里 2和 3的語義是一樣的):

            V v = { 0 };   
            V *pv = 0;  
            V __gc *pvgc = 0;  // 格式 (2) 是(3)的隱式格式  
            __box V* pvbx = 0;  // 必須是局部的  
            

            4.4.1 調(diào)用繼承的虛方法

            格式(1)是一個規(guī)范的值對象,并且它是相當容易理解的,除非有人試圖調(diào)用一個繼承虛方法,例如 ToString()。例如,

            v.ToString(); // 錯誤! 
            

            為了調(diào)用這個方法,因為在 V中它不可重寫,所以編譯器必須可以訪問基類的相關(guān)虛表。因為值類型是狀態(tài)內(nèi)存儲,沒有其虛表 (vptr) 的相關(guān)指針,所以這需要 v被裝箱。在原版語言設(shè)計中,隱式裝箱是不被支持的,程序員必須顯式聲明如下:

                        __box( v )->ToString(); // V1: 注意箭頭 
            

            該設(shè)計背后的主要動機是具有教育意義的 — 它希望使底層機制對于程序員可見,使得他能理解不在類型中提供實例的“代價”。如果 V包含一個 ToString實例,那么裝箱是不必要的。

            顯式裝箱對象的繁文縟節(jié),而不是裝箱本身的基本代價,在修訂版語言設(shè)計中被移除了。

               v.ToString(); // V2 
            

            但是代價是可能誤導類設(shè)計者在 V中不提供顯式 ToString方法的實例。首選隱式裝箱的原因是通常只有一個類設(shè)計者而有無數(shù)的類使用者,他們不能自由地修改 V來避免可能很麻煩的顯式裝箱。

            決定是否在值類中提供 ToString的一個重寫實例取決于它的使用頻率和位置。如果它很少被調(diào)用,那么這么定義很顯然沒什么好處。類似地,如果它在應(yīng)用程序的非性能區(qū)域被調(diào)用,那么添加它將不會對應(yīng)用程序的常規(guī)性能帶來可觀的提升?;蛘?,可以保留一個裝箱值的跟蹤句柄,通過該句柄的調(diào)用不會需要裝箱。

            4.4.2 值類不再有默認構(gòu)造函數(shù)

            類型的原版和修訂版語言設(shè)計之間的另外一個差異是取消了對默認構(gòu)造函數(shù)的支持。這是由于在執(zhí)行中,CLI可能創(chuàng)建一個類型的實例而不調(diào)用相關(guān)的默認構(gòu)造函數(shù)。換句話說,在 V1中,實際上并不能夠保證對類型中默認構(gòu)造函數(shù)的支持。由于缺乏保證,所以感覺完全去掉這個支持比在其應(yīng)用程序中保持不確定性更好。

            這并不像第一眼看上去那么壞。這是因為每個類型對象會被自動清零(每個類型會被初始化為其默認的值)。也就是說,局部實例的成員不會是未定義的。在這個意義上,缺少定義一個普通默認構(gòu)造函數(shù)的能力實際上根本不是一個損失 — 并且事實上在 CLI執(zhí)行時更加高效。

            問題發(fā)生在原版 V1語言的用戶定義了一個非普通默認構(gòu)造函數(shù)時。它沒有在修訂版V2語言設(shè)計中的對應(yīng)。構(gòu)造函數(shù)中的代碼將需要移植到一個命名的初始化方法,并且這個方法需要被用戶顯式調(diào)用。

            修訂版 V2 語言設(shè)計中的類型對象的聲明沒有變化。它的缺點是類型不能包裝本機類型,原因如下:

            1. 類型不支持析構(gòu)函數(shù)。換句話說,無法在對象生命周期結(jié)束時自動觸發(fā)一組行為。

            2. 本機類只能作為指針包含在托管類型中,然后在本機堆上進行分配。

            我們可能喜歡用類型(而不是引用類型)來包裝一個小的本機類來避免兩次堆分配:本機堆存放本機類型,CLI堆存放托管包裝。在類型中包裝一個本機類可以避免在托管堆的分配,但是無法自動回收本機堆上分配的內(nèi)存。引用類型是唯一可行的用于包裝非普通本機類的托管類型。

            4.4.3 內(nèi)部指針

            格式(2)和 (3)幾乎可以解決任何問題(即托管和本機)。因此,舉例來說,在原版語言設(shè)計中以下內(nèi)容都是被允許的:

                       // 來自于 4.4 節(jié) 
                  __value struct V { int i; }; 
                  __gc struct R { V vr; }; 
            V v = { 0 };   
            V *pv = 0;  
            V __gc *pvgc = 0;  // 格式 (2) 是 (3) 的隱式格式 
            __box V* pvbx = 0;  // 必須是局部的  
            R* r; 
            pv = &v;         //指向棧上的一個值類型 
            pv = __nogc new V;  //指向本機堆上的一個值類型 
            pv = pvgc;          // 我們不確定這指向什么位置 
            pv = pvbx;        // 指向托管堆上的裝箱值類型 
            pv = &r->vr;        //指向托管堆上一個引用類型中的值類型的內(nèi)部指針 
            

            這樣,一個 V*可以指向局部塊中的地址(因此可以成為虛引用);對于全局范圍來說,在本機堆內(nèi)(例如,如果它指向的對象已經(jīng)被刪除);在 CLI 堆內(nèi)(因此如果在垃圾回收期間會重新定位,則將進行跟蹤),以及在 CLI堆上的引用對象的內(nèi)部(顧名思義,內(nèi)部指針也透明地被跟蹤)。

            在原版語言設(shè)計中,無法分離 V*的本機方面。也就是說,它的處理具有包含性,處理指向一個托管堆上對象或者子對象的可能性。

            在修訂版語言設(shè)計中,類型指針有兩種類型:V*,位置局限于非 CLI 堆,和內(nèi)部指針 interior_ptr<V>,允許但是不強制一個地址位于傳統(tǒng)堆中。

            // 不能指向托管堆的地址 
            V *pv = 0; 
            // 可以但是不必須指向傳統(tǒng)堆之外的地址 
            interior_ptr<V> pvgc = nullptr; 
            

            原版語言中的格式 (2) (3)對應(yīng) interior_ptr<V>。格式 (4)是一個跟蹤句柄。它指向托管堆中裝箱的整個對象。這在修訂版語言中轉(zhuǎn)換成 V^

               V^ pvbx = nullptr; // __box V* pvbx = 0;  
            

            原版語言設(shè)計中的下列聲明在修訂版語言設(shè)計中都對應(yīng)到內(nèi)部指針。(它們是 System命名空間內(nèi)的值類型。)

            Int32 *pi;   => interior_ptr<Int32> pi; 
            Boolean *pb; => interior_ptr<Boolean> pb; 
            E *pe;       => interior_ptr<E> pe; // 枚舉 
            

            內(nèi)建類型不被認為是托管類型,雖然它們確實作為 System命名空間內(nèi)的類型的別名。因此,原版和修訂版語言的以下對應(yīng)是正確的:

            int * pi;     => int* pi; 
                  int __gc * pi => interior_ptr<int> pi; 
            

            當轉(zhuǎn)換現(xiàn)存程序中的 V*時,最保守的策略是總是將其轉(zhuǎn)換成為interior_ptr<V>。這就是它在原版語言中的處理方法。在修訂版語言中,程序員可以選擇通過指定 V*而不是使用內(nèi)部指針來限制一個值類型位于非托管堆。如果您在轉(zhuǎn)換程序時,可以傳遞閉包它的所有使用,并且確認沒有被賦值為托管堆中的地址,那么保留 V*就可以了。

            4.4.4 釘住指針

            垃圾回收器可能會在 CLI堆內(nèi)將堆上的對象移動到不同的位置,這通常發(fā)生在壓縮階段。(這個移動對跟蹤句柄、跟蹤引用和內(nèi)部指針來說不是問題,因為這些實體被透明的更新。但是,如果用戶在運行庫環(huán)境之外傳遞 CLI堆上對象的地址,這種移動就是個問題了。在這種情況下,這種不穩(wěn)定的對象移動很容易造成運行庫失敗。為了避免這種對象被移動,我們必須局部地將其釘住以備外部使用。

            在原版語言設(shè)計中,一個釘住指針是用 __pin關(guān)鍵字限定一個指針聲明來聲明的。以下是在原版語言規(guī)范的基礎(chǔ)上作了少量修改的一個示例:

            __gc struct H { int j; }; 
            int main()  
            { 
               H * h = new H; 
               int __pin * k = & h -> j; 
               // ... 
            }; 
            

            在新的語言設(shè)計中,一個釘住指針是以和內(nèi)部指針類似的語法聲明的。

            ref struct H 
            { 
            public: 
               int j; 
            }; 
            int main() 
            { 
               H^ h = gcnew H; 
               pin_ptr<int> k = &h->j; 
               // ... 
            } 
            

            修訂版語言下的釘住指針是一個內(nèi)部指針的特例。V1對釘住指針的限制仍舊存在。例如,它不能作為方法的參數(shù)或者返回類型使用,而且,它只能被聲明為一個局部對象。但是,一些額外的限制被添加到了修訂版語言設(shè)計中:

            1. 釘住指針的默認值是 nullptr,而不是 0。pin_ptr<>不能被初始化或者賦值為 0。現(xiàn)存代碼中賦值為 0的都需要改為 nullptr。

            2. V1下的釘住指針允許指向整個對象,如下面引用自原版語言規(guī)范的示例所述:

                     __gc struct H { int j; }; 
            void f( G * g )  
            { 
                        H __pin * pH = new H;    
                         g->incr(& pH -> j);    
            }; 
            

            在修訂版語言中,釘住 new 表達式返回的整個對象是不被支持的。確切地說,是需要釘住內(nèi)部成員的地址。舉例來說:

            void f( G^ g ) 
            { 
               H ^ph = gcnew H; 
               pin_ptr<int> pj = &ph->j; 
               g->incr(  pj ); 
            } 
            

            5. 語言變化概要

            本節(jié)中描述的更改某種意義上是語言雜記。本節(jié)包含處理字符串的修改,省略號和參數(shù)屬性的重載解決方案的修改,從 typeof typeid的修改,以及一個新的強制轉(zhuǎn)換標記 safe_cast的介紹。

            5.1 字符串

            在原版語言設(shè)計中,托管字符串是通過為字符串添加前綴 S的方式指明的。例如:

               String *ps1 = "hello"; 
               String *ps2 = S"goodbye"; 
            

            兩個初始化之間的性能開銷差別并不小,如下面通過 ildasm看到的的 CIL表示所示:

            // String *ps1 = "hello"; 
            ldsflda    valuetype $ArrayType$0xd61117dd 
                 modopt([Microsoft.VisualC]Microsoft.VisualC.IsConstModifier)  
                 '?A0xbdde7aca.unnamed-global-0' 
            newobj instance void [mscorlib]System.String::.ctor(int8*) 
            stloc.0 
            // String *ps2 = S"goodbye"; 
            ldstr      "goodbye" 
            stloc.0 
            

            記得(或者學習)在字符串前加上前綴 S就會有可觀的性能節(jié)省。在修訂版的 V2語言中,字符串的處理被透明化,由使用的上下文決定。S不再需要被指定。

            在我們需要顯式告訴編譯器使用哪種解釋時情況又是怎樣呢?在這樣的情況下,我們使用顯式的轉(zhuǎn)換。例如:

               f( safe_cast<String^>("ABC") ); 
            

            此外,字符串現(xiàn)在將一個 String與一個普通轉(zhuǎn)換相匹配而不是匹配一個標準轉(zhuǎn)換,雖然這看起來影響不大,但是卻改變了包含 String constchar*作為區(qū)別形式參數(shù)的重載函數(shù)集的解析方式。一度被解析為 const char*實例的解析現(xiàn)在被標志為有歧義的。例如:

            void f(const char*); 
            void f(String^); 
            // v1: f( const char* ); 
            // v2: 錯誤:有歧義... 
            f("ABC");  
            

            這里發(fā)生了什么?為什么有區(qū)別?因為程序中存在不止一個以 f 為名稱的實例,所以需要將函數(shù)重載解析算法應(yīng)用于調(diào)用。正式的函數(shù)重載解析包含以下三個步驟:

            1. 搜集候選函數(shù)。候選函數(shù)是作用域內(nèi)字面上匹配所調(diào)用函數(shù)的名稱的方法。例如,因為 My()是通過 R的一個實例調(diào)用的,所有不是 R(或其基類層次結(jié)構(gòu))成員的命名函數(shù) My都不是候選函數(shù)。在我們的示例中,有兩個候選函數(shù)。這就是名為 MyR函數(shù)的兩個成員。

            2. 候選函數(shù)中的可行函數(shù)集。一個可行函數(shù)是可以用調(diào)用中指定的參數(shù)調(diào)用的函數(shù)(如果給定參數(shù)的數(shù)量和類型)。如果可行函數(shù)集為空,那么調(diào)用失敗。

            3. 選擇表示最匹配調(diào)用的函數(shù)。這是通過對應(yīng)用于從參數(shù)到可行函數(shù)參數(shù)類型的轉(zhuǎn)換進行定級來實現(xiàn)的。對只有一個參數(shù)的函數(shù)來說,這相對簡單一些;但是多參數(shù)函數(shù)的情況下有點復雜。如果沒有最佳匹配,那么調(diào)用在該階段會失敗。換句話說,如果從實參類型到形參類型所需的轉(zhuǎn)換都同樣好,那么調(diào)用被標記為有歧義的。

            在原版語言設(shè)計中,作為最佳匹配,該調(diào)用的解析調(diào)用 constchar*實例。在 V2中,“abc”到 constchar* String^匹配所需的轉(zhuǎn)換現(xiàn)在是等價的 — 換句話說,同樣好 — 因此調(diào)用被標記為壞的 — 也就是說,有歧義的。

            這導致我們思考以下兩個問題:

            1. 實際參數(shù)“abc”的類型是什么?

            2. 判斷一個類型轉(zhuǎn)換優(yōu)于另一個類型轉(zhuǎn)換的算法是什么?

            字符串“abc”的類型是 constchar[4]— 記住,每個字符串的末尾有一個隱式的 null 終止符。

            判斷一個類型轉(zhuǎn)換優(yōu)于另一個的算法涉及到將可能的類型轉(zhuǎn)換放在層次結(jié)構(gòu)中。以下是我對這個層次結(jié)構(gòu)的理解 — 當然,所有這些轉(zhuǎn)換都是隱式的。使用顯式轉(zhuǎn)換標記會重新定義層次結(jié)構(gòu),就像圓括號重新定義表達式的運算次序一樣。

            • 一個精確匹配是最好的。令人驚異的是,對于精確匹配的參數(shù)來說,不必精確匹配參數(shù)類型,只需要足夠接近。這是理解本例的原理和語言如何進行修改的關(guān)鍵。

            • 提升優(yōu)于標準轉(zhuǎn)換。例如,short int int的提升優(yōu)于 int double的轉(zhuǎn)換。

            • 標準轉(zhuǎn)換優(yōu)于裝箱轉(zhuǎn)換。例如,int double的轉(zhuǎn)換優(yōu)于 int Object的裝箱。

            • 裝箱轉(zhuǎn)換優(yōu)于隱式用戶自定義轉(zhuǎn)換。例如,int Object的裝箱優(yōu)于應(yīng)用 SmallInt值類的轉(zhuǎn)換操作符。

            • 隱式用戶定義轉(zhuǎn)換優(yōu)于根本沒有轉(zhuǎn)換。隱式用戶定義轉(zhuǎn)換是錯誤之前的最后一個出口(警告:形式參數(shù)中的位置可能包含一個參數(shù)數(shù)組或者省略號)。

            這樣,為什么精確匹配不一定會確定一個匹配?舉例來說,const char[4]并不精確匹配 const char*或者 String^,但是在我們的示例中,兩個不一致的精確匹配之間仍然存在歧義!

            精確匹配發(fā)生時,包含一系列小轉(zhuǎn)換。在 ISO-C++中有 4 個普通轉(zhuǎn)換可以使用,并且仍舊滿足精確匹配,其中三個被稱為左值轉(zhuǎn)換。第四個轉(zhuǎn)換被稱為限定轉(zhuǎn)換。三個左值轉(zhuǎn)換比需要限定轉(zhuǎn)換的精確匹配更優(yōu)越。

            左值轉(zhuǎn)換的一種形式是本機-數(shù)組-指針的轉(zhuǎn)換。這就是將 const char[4]匹配到 const char*所發(fā)生的事情。因此,從 My("abc")到 My(const char*)的匹配是一個精確匹配。在 C++/CLI語言的早期版本中,這實際上是最佳轉(zhuǎn)換。

            因為編譯器要將調(diào)用標記為有歧義的,所以這要求一個從 const char[4] String^的轉(zhuǎn)換也通過普通轉(zhuǎn)換成為一個精確匹配。這就是 V2 中新加入的更改。并且這也是調(diào)用被標記為有歧義的原因。

            5.2 參數(shù)數(shù)組和省略號

            在原版語言設(shè)計和 VisualStudio2005中即將發(fā)布的 V2語言中都沒有對 C#和 Visual Basic .NET 支持的參數(shù)數(shù)組的顯式支持。作為替代,用一個屬性標記普通數(shù)組如下:

            void Trace1( String* format, [ParamArray]Object* args[] ); 
            void Trace2( String* format, Object* args[] ); 
            

            雖然這看起來都一樣,但是 ParamArray 屬性在 C#或者其他 CLI語言中將其標記為每個調(diào)用獲取可變數(shù)量元素的數(shù)組。在重載函數(shù)集合的解析中,修訂版語言針對原版進行了程序行為的更改,其中一個實例聲明了省略號,另一個聲明了 ParamArray 屬性,如以下 Artur Laksberg 提供的示例所示:

            int My(...); // 1 
            int My( [ParamArray] Int32[] ); // 2 
            

            在原版語言設(shè)計中,省略號的解析優(yōu)先于屬性,這是有道理的,因為屬性不是語言的正式部分。然而在 V2中,現(xiàn)在語言直接支持參數(shù)數(shù)組,所以它優(yōu)先于省略號,因為它是更強類型的。因此,在原版語言中,調(diào)用

            My( 1, 2 );     
            

            解析至 My(...),而在修訂版語言中,它解析至 ParamArray 實例。如果應(yīng)用程序的行為依賴于省略號實例的調(diào)用優(yōu)先于 ParamArray 的調(diào)用,那么您需要修改符號或者調(diào)用。

            5.3 typeof 改為 T::typeid

            在原版語言設(shè)計中,__typeof()操作符在傳遞一個托管類型的名稱時返回相關(guān)的 Type*對象,例如:

            //創(chuàng)建并初始化一個新的 Array 實例。 
            Array* myIntArray =  
                   Array::CreateInstance( __typeof(Int32), 5 ); 
            

            在修訂版語言設(shè)計中,__typeof被另一種 typeid形式替代,它在指定一個托管類型時返回一個 Type^

            //創(chuàng)建并初始化一個新的 Array 實例。 
            Array^ myIntArray =  
             Array::CreateInstance( Int32::typeid, 5 ); 
            

            5.4 強制轉(zhuǎn)換符號和 safe_cast<> 簡介

            注意,這是較為冗長的一節(jié),所以一些耐不住性子的人可以快速跳到末尾來閱讀實際更改的說明。

            修改一個已經(jīng)存在的結(jié)構(gòu)是完全不同的 — 在某種意義上,比編寫原始的結(jié)構(gòu)更加困艱難;自由度更少,以及解決方案趨于理想重構(gòu)和實際上對現(xiàn)存的結(jié)構(gòu)依賴性之間的妥協(xié)。舉例來說,如果您曾經(jīng)進行過排版,您就會知道,由于需要將重新格式化限定在當前頁中,因此對現(xiàn)存頁的更正就有所限制;您不能允許文本溢出到后面的頁面中,這樣您就不能添加或者刪節(jié)太多(或太少)內(nèi)容,而且經(jīng)常會讓人感覺到更正是為了適合版面,從而其意義有所妥協(xié)。

            語言擴展是另外一個例子。回到 20 世紀 90 年代初,面向?qū)ο缶幊坛蔀橐粋€重要的范型,對 C++ 中類型安全的向下轉(zhuǎn)換的需求逐漸增大。向下轉(zhuǎn)換是用戶對基類指針、指針引用,或派生類的引用的顯式轉(zhuǎn)換。向下轉(zhuǎn)換需要一個顯式的轉(zhuǎn)換,這是因為如果基類指針不是派生類對象,程序很可能做出一些很不好的事情。問題在于基類指針的實際類型是運行庫的一個方面,因此編譯器無法檢查它。或者換句話說,向下類型轉(zhuǎn)換就像一個虛函數(shù)調(diào)用,需要某種形式的動態(tài)解析。這產(chǎn)生了兩個問題:

            1. 為什么在面向?qū)ο蠓缎椭行枰蛳骂愋娃D(zhuǎn)換?難道虛函數(shù)機制不適合所有情況?也就是說,為什么不能聲明任何對向下轉(zhuǎn)換(或者任何類型的轉(zhuǎn)換)的需要是程序員的設(shè)計失?。?/p>

            2. 為什么支持向下轉(zhuǎn)換成為 C++的一個問題?畢竟,這在任何諸如 Smalltalk(或者隨后的 Java和 C#)的面向?qū)ο笳Z言中都不是問題?為什么在 C++中支持向下轉(zhuǎn)換有困難?

            虛函數(shù)代表類型家族中常見的一個依賴于類型的算法(我沒有考慮接口,這在 ISO-C++中不被支持,但是在 C++/CLI中可用,并且代表一個有趣的替代設(shè)計方案)。該類型家族設(shè)計的典型代表是一個類層次結(jié)構(gòu),其中具有一個聲明了通用接口(虛函數(shù))的抽象基類的,以及一組具體派生類,代表應(yīng)用程序域中實際類型家族。

            舉例來說,一個電腦成像 (CGI) 應(yīng)用程序域中一個輕量級的的層次結(jié)構(gòu),會具有一些諸如 colorintensity、position、on、off等等的共同屬性??梢栽谀硞€圖像中撒下幾束光,并且通過通用接口控制它們,而不用擔心光到底是聚光、平行光、全向光(如太陽光),還是通過擋光板的光。在這種情況下,向下轉(zhuǎn)換為一個特定的光類型來實現(xiàn)其虛接口是不必要的,因為所有的方式都一樣,所以是不明智的。但是,在生產(chǎn)環(huán)境中,情況不總是一樣的;很多情況下,考慮的是速度;程序員可能會選擇向下轉(zhuǎn)換,然后顯式調(diào)用每個方法,如果這樣,調(diào)用的內(nèi)聯(lián)直接執(zhí)行會替代通過虛函數(shù)機制執(zhí)行。

            因此,在 C++中使用向下轉(zhuǎn)換的一個原因是抑制虛函數(shù)機制而獲得可觀的運行庫性能(注意,將手動優(yōu)化進行自動化是研究的活躍領(lǐng)域。但是這比替換 register或者 inline關(guān)鍵字的顯式使用更加困難)。

            使用向下轉(zhuǎn)換的另一個原因是多態(tài)性的雙重屬性。關(guān)于多態(tài)的一個觀點是將它區(qū)分成被動和動態(tài)兩種形式。

            一個虛調(diào)用(和向下轉(zhuǎn)換功能)代表多態(tài)性的動態(tài)使用:在程序執(zhí)行過程中實現(xiàn)一個操作,該操作基于特殊實例的基類指針的實際類型。

            但是,將一個派生類對象賦值給其基類指針是多態(tài)性的被動形式;這里將多態(tài)性作為一個傳輸機制。這是 Object類的主要用途,例如在普及的 CLI 中就是這樣。作為被動形式使用時,用于傳輸和存儲的基類指針通常提供一個過于抽象的接口。舉例來說,Object通過其接口提供了大約 5 個方法;任何更明確的行為需要一個顯式的向下轉(zhuǎn)換。例如,如果我們希望調(diào)整聚光燈的角度或者照射角度,我們會需要顯式的向下轉(zhuǎn)換。子類型家族中的虛接口不能是其許多子成員的所有可能方法的超集,所以面向?qū)ο笳Z言中向下轉(zhuǎn)換功能總是必要的。

            如果一個安全的向下轉(zhuǎn)換功能在面向?qū)ο蟮恼Z言中是必要的,那么為什么 C++花了這么久的時間來添加該功能?問題在于如何使指針的運行庫類型信息可用。對于虛函數(shù),就像大多數(shù)人目前了解的一樣,運行庫信息是編譯器分兩部分建立的:(a) 類對象包含一個額外的虛表指針成員(在類對象的開頭或者末尾;這是它本身的一個有趣的歷史),它指向適當?shù)奶摫?— 所以,舉例來說,一個聚光對象指向一個聚光虛表,對平行光是平行光虛表,等等;以及 (b) 每個虛函數(shù)在表中有一個相關(guān)的固定位置,并且實際調(diào)用的實例由表中存儲的地址來表示。因此,舉例來說,虛析構(gòu)函數(shù) ~Light可能與位置 0相關(guān)聯(lián),Color與位置 1相關(guān)聯(lián),等等。這是一個如不靈活即有效的策略,因為它是在編譯時設(shè)置的,而且代表最小的開銷。

            現(xiàn)在的問題是如何使類型信息可用于指針而不改變 C++指針的大小,方法是再添加一個地址,或者直接添加一些類型編碼。這不可能被那些選擇不進行面向?qū)ο蠓缎途幊痰某绦騿T(和程序) — 他們?nèi)耘f是具有很大影響的用戶團體 — 接受。另外一個可能性是為多態(tài)類類型引入一個特定的指針,但這將造成可怕的混亂,并且使得混合兩者變得非常困難 — 特別是在指針算法問題方面。維護將每個指針關(guān)聯(lián)到當前相關(guān)類型的運行庫表,以及動態(tài)對其進行更新也是不可接受的。

            現(xiàn)在的問題是,兩個用戶社區(qū)有不同但是合理的編程期望。解決方案需要在兩個社區(qū)之間進行妥協(xié),不但允許每個社區(qū)的期望而且也允許互操作能力得以實現(xiàn)。這意味著兩個社區(qū)提供的方案看起來都不可取,而最終實現(xiàn)的解決方案很可能并不完美。實際的解決方案圍繞多態(tài)類的定義:多態(tài)類是一個包含虛函數(shù)的類。一個多態(tài)類支持動態(tài)類型安全的向下轉(zhuǎn)換。這解決了“以地址的形式維護指針”的問題,因為所有多態(tài)類包含額外的指針成員,指向其相關(guān)虛表。因此,相關(guān)類型信息可以保存在一個擴展的虛表結(jié)構(gòu)中。類型安全向下轉(zhuǎn)換的開銷是(幾乎)限制了功能的使用者的范圍。

            關(guān)于類型安全的向下轉(zhuǎn)換的下一個問題是它的語法。因為它是一個強制轉(zhuǎn)換,ISO-C++協(xié)會的原意是使用未裝飾的強制轉(zhuǎn)換語法,因此編寫如下示例代碼:

               spot = ( SpotLight* ) plight; 
            

            但是這被委員會否決了,因為這不允許用戶控制強制轉(zhuǎn)換的代價。如果動態(tài)類型安全的向下轉(zhuǎn)換具有前面的不安全但是是靜態(tài)的標記,那么它將成為一個替代方案,而且用戶無法在不必要或者代價太大時降低運行庫的開銷。

            通常,C++中總有機制抑制編譯器支持的功能。例如,我們可以通過使用類作用域操作符 Box::rotate(angle)或者通過類對象(而不是通過這個類的指針或者引用)調(diào)用虛函數(shù)來關(guān)閉虛函數(shù)機制 — 后面一個抑制是語言不需要的,但是是一些實現(xiàn)問題所必需的……它類似于以如下形式在聲明時構(gòu)造一個臨時對象:

               //編譯器可以自由優(yōu)化掉這個臨時對象... 
            X x = X::X( 10 );  
            

            因此提議被打回重新考慮,很多替代的符號被考慮過,而最后提交給委員會的是 (?type)形式,表示它的不確定 — 也就是動態(tài) — 本質(zhì)。這為用戶提供了在兩種形式 — 靜態(tài)或者動態(tài) — 之間切換的能力,但是沒有人滿意。所以它又回到制圖板。第三個,也是成功的一個標記是現(xiàn)在的標準 dynamic_cast<type>,它被通用化為四個新風格的強制轉(zhuǎn)換標記集合。

            在 ISO-C++中,dynamic_cast在應(yīng)用到一個不合適的指針類型時返回 0,并且在應(yīng)用到一個引用類型時引發(fā)一個 std::bad_cast異常。在原版語言設(shè)計中,將 dynamic_cast應(yīng)用到一個托管引用類型(因為它的指針表達方法)總是返回 0。__try_cast<type>作為一個引發(fā) dynamic_cast變體的異常的類似被引入,但是它在強制轉(zhuǎn)換失敗時引發(fā)System::InvalidCastException異常。

            public __gc class ItemVerb; 
            public __gc class ItemVerbCollection 
            { 
            public: 
                ItemVerb *EnsureVerbArray() [] 
                { 
                 return __try_cast<ItemVerb *[]> 
                         (verbList->ToArray(__typeof(ItemVerb *))); 
                } 
            }; 
            

            在修訂版語言中,__try_cast被重新轉(zhuǎn)換為 safe_cast。以下是修訂版語言中同樣的代碼片斷:

            using namespace stdcli::language; 
            public ref class ItemVerb; 
            public ref class ItemVerbCollection 
            { 
            public: 
                array<ItemVerb^>^ EnsureVerbArray() 
                { 
               return safe_cast<array<ItemVerb^>^> 
                        ( verbList->ToArray( ItemVerb::typeid )); 
               } 
            }; 
            

            在托管領(lǐng)域,限制程序員以代碼不可驗證的方式在類型間進行轉(zhuǎn)換的能力,從而允許可驗證代碼是很重要的。這是 C++/CLI 代表的動態(tài)編程范型的一個關(guān)鍵部分。由于這個原因,舊風格類型轉(zhuǎn)換的實例作為運行庫轉(zhuǎn)換被內(nèi)部重新轉(zhuǎn)換,所以,舉例來說:

               //內(nèi)部轉(zhuǎn)換為上面的等價的 safe_cast 表達式 
            ( array<ItemVerb^>^ ) verbList->ToArray( ItemVerb::typeid );  
            

            另一方面,因為多態(tài)提供了動態(tài)和被動兩種模式,有時有必要執(zhí)行一個向下類型轉(zhuǎn)換,只是為了獲得對子類型的非虛 API的訪問能力。舉例來說,當指向?qū)哟沃腥魏晤愋偷念惖某蓡T(使用被動多態(tài)性作為傳輸機制),但是在一個特定程序上下文中的實際實例已知的時候,可能發(fā)生這種情況。在這種情況下,系統(tǒng)程序員強烈的感覺到,進行類型轉(zhuǎn)換的運行庫檢查具有無法接受的性能開銷。如果 C++/CLI 作為托管系統(tǒng)編程語言,它必須提供一些方法來允許編譯時(即靜態(tài))向下轉(zhuǎn)換。這就是為什么在修訂版語言中 static_cast標記的使用仍允許保持為一個編譯時向下轉(zhuǎn)換的原因。

            // OK:在編譯時執(zhí)行的強制轉(zhuǎn)換 
            // 沒有運行時的類型正確性檢查 
            static_cast< array<ItemVerb^>^>(  
                         verbList->ToArray( ItemVerb::typeid ));  
            

            當然,問題是無法保證程序員執(zhí)行的 static_cast是正確和善意的。換句話說,無法保證托管代碼的可驗證性。這是在動態(tài)編程范型下比本機環(huán)境更迫切的一個考慮,但是不足以在系統(tǒng)編程語言中禁用用戶切換靜態(tài)和運行時類型轉(zhuǎn)換的能力。

            有一個 C++/CLI的性能陷阱和缺陷需要注意,在本機編程中,舊風格的強制轉(zhuǎn)換標記和新風格的 static_cast標記在性能上沒有區(qū)別。但是在新語言設(shè)計中,舊風格強制轉(zhuǎn)換標記的性能開銷比新風格static_cast標記的性能開銷更加昂貴,因為編譯器需要將舊風格標記的使用內(nèi)部轉(zhuǎn)換為引發(fā)異常的運行時檢查。而且,它還更改了代碼的執(zhí)行配置文件,因為它導致在程序中引入一個未捕捉的異常 — 可能是智能的,但是如果使用 static_cast標記,那么同樣的錯誤將不會導致該異常。可能有人有異議,好的,這將有助于刺激用戶使用新風格的標記。但是只在它失敗的時候才會這樣;否則,它只會導致使用舊風格標記的程序運行更加緩慢,而沒有可以理解清楚的原因,如以下 C 程序員所犯的錯誤:

            // 缺陷 # 1:  
            // 初始化可以避免一個臨時類對象的創(chuàng)建,而賦值不行 
            Matrix m;      
            m = another_matrix;   
            // 缺陷# 2: 類對象的聲明遠離其使用 
            Matrix m( 2000, 2000 ), n( 2000, 2000 ); 
            if ( ! mumble ) return; 
            

            附錄:推動修訂版語言設(shè)計

            原版和修訂版語言設(shè)計之間最顯著和引人注目的更改可能是托管引用類型聲明的更改:

            // 原版語言 
            Object * obj = 0; 
            // 修訂版語言 
            Object ^ obj = nullptr; 
            

            看到這段代碼時主要會提出兩個問題:為什么帽子(^符號)在微軟的走廊里家喻戶曉,但是,更根本的是,為什么要新的語法?為什么原版語言設(shè)計不能被清理以減少侵略性,而推薦公認咄咄逼人的、陌生的修訂版 C++/CLI語言設(shè)計?

            C++是基于面向機器的系統(tǒng)視圖建立的。雖然它支持一個高級的類型系統(tǒng),但是總有回避它的機制,這些機制總是導致對機器的依賴性。當事態(tài)嚴重,而且用戶努力去做一些不可思議的事的時候,他們會繞過應(yīng)用程序的抽象過程,重新將類型分離為地址和偏移。

            CLI是操作系統(tǒng)和應(yīng)用程序之間運行的一個軟件抽象層。當事態(tài)嚴重時,用戶會毫無根據(jù)地逐字反思執(zhí)行環(huán)境、查詢、代碼和對象創(chuàng)建問題,跳過而不是遵循類型系統(tǒng),但是這個經(jīng)驗對于習慣腳踏實地的人來說會是一團糟。

            例如,下面的內(nèi)容是什么意思?

            T t;  
            

            好的,在 ISO-C++中,不管 T的本質(zhì)是什么,我們都可以確認下列特性:(1) 有一個與 t相關(guān)的字節(jié)的編譯時內(nèi)存委托等于 sizeof(T);(2) 在程序中 t的作用域內(nèi),這個與 t關(guān)聯(lián)的內(nèi)存獨立于其他所有對象;(3) 內(nèi)存直接保持與 t相關(guān)的狀態(tài)/值;以及 (4) 內(nèi)存和狀態(tài)在 的作用域內(nèi)存在。

            下列特性的結(jié)果是什么?

            第 (1) 項告訴我們 t不能是多態(tài)的。也就是說,它不能代表一個繼承層次中的一系列類型。換句話說,一個多態(tài)類型不能有一個編譯時的內(nèi)存分配,除非派生實例沒有額外的內(nèi)存需求。無論 T是一個基本類型還是一個復雜層次的基類,這都成立。

            C++中的多態(tài)類型只可能在類型限定為指針 (T*)或者引用 (T&) 才可用 — 也就是說,如果聲明只是間接引用一個 T的對象。如果:

               Base b = *new Derived; 
            

            那么 b并不指向一個位于本機堆上的 Derived對象。值 b沒有和 new 表達式分配的Derived對象關(guān)聯(lián),而 Derived對象的 Base部分被截斷,并且按位復制到獨立的基于棧的實例 b。這在 CLI對象模型中實際上沒有對應(yīng)的描述。

            為了將資源提交延遲到運行時進行,C++ 顯式支持兩種間接形式: 
            
            指針:   T *pt = 0;  
            引用:   T &rt = *pt;  
            

            指針和 C++對象模型一致。在

            T *pt = 0;  
            

            中,pt直接保存一個 size_t類型的值,該值具有固定的大小和作用域。語法詞匯習慣于在指針的直接使用和指向對象的間接使用之間切換。眾所周知,*pt++在何種模式應(yīng)用于什么/何時應(yīng)用/如何應(yīng)用這個問題上具有歧義。

            引用為看起來有些復雜的指針詞匯提供了一種簡單的語法,同時保持其效率。

            Matrix operator+( const Matrix&, const Matrix& );  
            Matrix m3 = m1 + m2; 
            

            引用并不在直接和間接模式之間切換;而是在兩者之間進行階段轉(zhuǎn)移:(a) 初始時,它們被直接操作;但是 (b) 在所有后續(xù)的使用中,它們是透明的。

            某種意義上說,引用代表了 C++ 對象模型物理學的一個奇異量子:(a) 它們占用空間,但是除了臨時對象之外,它們并沒有實體;(b) 它們在賦值時使用深拷貝 (deep copy),而在初始化時使用淺拷貝 (shallow copy);以及 (c) 與 const對象不同,參數(shù)實際上沒有實體。雖然在 ISO-C++中它們除了用于函數(shù)參數(shù)之外沒有太多的用途,但是在語言修訂版方面,它們十分具有靈感。

            C++.NET 設(shè)計難題

            字面上,對于C++擴展支持 CLI的每一方面,問題總是歸結(jié)到“我們?nèi)绾螌⒐舱Z言基礎(chǔ)結(jié)構(gòu) (Common Language Infrastructure,CLI) 的這個(或者那個)方面集成到 C++ 中,使它 (a) 讓 C++程序員感覺自然,以及 (b) 感覺像 CLI自身的一個一流的功能”?;谶@些考慮,這個權(quán)衡在原版語言設(shè)計中沒有實現(xiàn)。

            讀者的語言設(shè)計難題

            因此,為了讓你看到一些步驟,這里指出我們所面臨的難題:我們?nèi)绾温暶骱褪褂靡粋€ CLI引用類型?它和 C++對象模型有顯著區(qū)別:不同的內(nèi)存模型(垃圾回收),不同的復制語義(淺拷貝),不同的繼承模型(一體化,基于 Object,只有對接口有額外的支持時才支持單繼承)。

            C++設(shè)計的原版托管擴展

            在 C++中支持 CLI引用類型的基礎(chǔ)設(shè)計選擇就是決定是保留現(xiàn)存的語言,還是擴展語言,因而打破現(xiàn)有標準。

            您會作何選擇?每個選擇都會被指責。標準歸結(jié)為一個人是否相信額外的語言支持代表域抽象(考慮并行和線程)或者范型轉(zhuǎn)移(考慮面向?qū)ο蟮念愋汀宇愋完P(guān)系和泛型)。

            如果您相信額外的語言支持只代表另一個域抽象,您將會選擇保留現(xiàn)存語言。如果您了解到額外的語言支持代表編程范型的轉(zhuǎn)移,您會擴展語言。

            簡而言之,原版語言設(shè)計認為額外的語言支持只是一個域抽象 — 這被笨拙的稱為托管擴展— 因此邏輯上后續(xù)的設(shè)計選擇是保持現(xiàn)存語言。

            一旦我們致力于保持現(xiàn)存語言,只有三個替代的方法實際上可行 — 記住,我將討論限制在簡單的“如何表示一個 CLI引用類型”上:

            1. 讓語言支持透明化。編譯器會根據(jù)上下文決定語義。有歧義時會產(chǎn)生一個錯誤,并且用戶會通過一些特殊語法決定上下文的含義(作為類比,可以考慮根據(jù)層次結(jié)構(gòu)的先后順序重載函數(shù))。

            2. 以庫的方式添加域抽象支持(考慮將標準模板庫作為可能的模型)。

            3. 重用某些現(xiàn)存的語言元素,基于附帶規(guī)范中的描述,根據(jù)上下文限制其允許的用途和行為。(考慮虛基類的初始化和類型強制轉(zhuǎn)換的語義,或者函數(shù)中、文件范圍內(nèi)和類聲明中 static 關(guān)鍵字的多種用途)。

            每個人的首選都是第 1 項。“它只是和語言中其他內(nèi)容一樣,只是少許不同。讓編譯器判斷就好了。”這里很大的成功在于對于現(xiàn)存代碼來說,所有內(nèi)容對用戶都是透明的。將您現(xiàn)有的應(yīng)用程序拿出來,添加一兩個對象,編譯,然后,ta-dah,它就完成了。使用方便,操作簡單。在類型和源代碼方面完全可以互用。沒有人會質(zhì)疑該方案不是理想方案,很大程度上就像沒有人爭論永動機的理想性一樣。在物理學上,這個問題的障礙是熱力學第二定律,以及熵的存在。在一個多范型編程語言中,規(guī)則有顯著不同,但是系統(tǒng)的瓦解是一樣明確的。

            在一個多范型編程語言中,事情在各自的范型內(nèi)運作相當良好,但是在范型被不正確地混合時趨于崩潰,導致程序崩潰或者更壞,運行但是產(chǎn)生錯誤的結(jié)果。這在支持獨立的基于對象和多態(tài)的面向對象的類編程中最常見。切片使得每個 C++新手的編程變得混亂:

            DerivedClass dc;    // 一個對象 
            BaseClass &bc = dc; // OK:bc 真的是一個 dc 
            BaseClass bc2 = dc; // OK:但是 dc 可能被切片以適應(yīng) bc2 
            

            因此,打比方來說,語言設(shè)計的第二定律是讓行為不同的事物看起來具有足夠的差異以提醒用戶,在他或者她編程時盡量避免,嗯,一團糟。我們習慣于用兩個小時介紹中的半個小時來開始 C 程序員對指針引用之間差異理解的第一步,而且大量的 C++ 程序員仍不能清楚地描述何時使用引用聲明,何時使用指針,以及原因。

            這些迷惑無可否認地使編程更困難,并且在簡單地去除它們和其支持所提供的現(xiàn)實功能之間總有一個重要的權(quán)衡。并且它們的差異在于設(shè)計的透明度,以及在于它們是否實用。而且通常設(shè)計是通過類推實現(xiàn)的。當指向類成員的指針被引入到語言中時,成員選擇操作符被擴展了(例如從 -> 到 ->*),并且指向函數(shù)語法的指針被類似的擴展了(從 int (*pf)()int(X::*pf)())。同樣地,類的靜態(tài)數(shù)據(jù)成員的初始化也被擴展了,等等。

            引用對操作符重載的支持是必須的。您可以得到直觀的語法

                  Matrix c = a + b;  // Matrix operator+( Matrix lhs, Matrix rhs );  
                 c = a + b + c;     
            

            但是這很難說是一個有效的實現(xiàn)。C 語言指針的替代方案 — 這提供了效率 —被其非直觀語法所分隔:

            // Matrix operator+( const Matrix* lhs, const Matrix* rhs );  
            Matrix c = &a + &b;   
            c = &( &a + &b ) + &c; 
            

            引入引用提供了指針的效率,但是保留了直接訪問類型的簡單語義。它的聲明類似于指針,并且易于理解。

                   // Matrix operator+( const Matrix& lhs, const Matrix& rhs ); 
                Matrix c = a + b;    
            

            但是對習慣于使用指針的程序員來說,它的語義行為還是令人迷惑。

            這樣,問題就是,對于習慣 C++對象的靜態(tài)行為的 C++程序員來說,理解和正確使用托管引用類型會有多么容易?而且理所當然地,什么可以幫助程序員在這方面進行最好的設(shè)計?

            我們覺得兩個類型的差別足以保證特別處理,因此我們排除了選項 #1。甚至在修訂版語言中,我們?nèi)灾С诌@個選擇。那些爭論這個選擇的人(一度包括我們中的大部分)只是沒有坐下來深入理解這個問題。這不是指責,只是事實。因此,如果您考慮前面的設(shè)計難題并且提出一個透明的設(shè)計,那么我會斷定,根據(jù)我們的經(jīng)驗,那不是一個可行的解決方案,我堅持這一點。

            第二和第三個選項,或者采取一個庫設(shè)計,或者重用現(xiàn)有的語言元素,都是可行的,并且各有所長,因為 Stroustrup 的 cfront源代碼很容易獲得,所以在貝爾實驗室中庫解決方案連篇累牘。它在某種程度上曾經(jīng)是大眾化的(HCE)。甲修改 cfront來添加并行性,乙修改 cfront來添加他們喜歡的域擴展,每個人都炫耀其新的 C++ 語言修改版,而 Stroustrup 的正確回答是這最好在一個庫中進行處理。

            這樣,為什么我們沒有選擇一個庫解決方案?嗯,部分原因只是一個感覺上的問題。就像我們感覺兩種類型的差異足以保證特別處理一樣,我們感覺兩種類型的類似之處足以保證類似地處理。一個庫類型在很多方面表現(xiàn)得像語言中的內(nèi)建類型一樣,但是它實際上不是。它不是一個一級的語言。我們感覺,我們必須盡力使引用類型成為語言的一級元素,因此,我們選擇不部署庫解決方案。這個選擇仍存在爭議。

            這樣,為了引用類型和現(xiàn)存類型對象模型太過不同的感覺而拋棄了透明的解決方案,并且為了引用類型和現(xiàn)存類型對象模型需要在語言中有同等地位的感覺而拋棄了庫解決方案,我們剩下的問題是如何將引用類型集成到現(xiàn)存語言中。

            如果我們從零開始,我們當然可以實現(xiàn)任何所希望的,從而提供一個統(tǒng)一的類型系統(tǒng),并且 — 至少在我們修改了這個類型系統(tǒng)之前 — 我們做的任何事情都會煥然一新。這通常是我們在生產(chǎn)和技術(shù)中所做的。但是,我們被限制了,這是福也是禍。我們不能拋棄現(xiàn)存的 C++對象模型,所以我們做的任何事情必須與它兼容。在原版語言設(shè)計中,我們進一步限制了自己,不引入任何新的標記;因此;我們必須使用已有的標記。這并未給我們提供多少靈活度。

            因此,為了切入重點,在原版語言設(shè)計中,假設(shè)給定剛才列舉過的限制(希望沒有太多的混淆),語言設(shè)計者覺得唯一可行的托管引用類型的表示方法是重用現(xiàn)存指針語法 — 引用并不是足夠靈活的,因為他們不能被重新賦值,并且他們不能不引用任何對象:

            // 所有在托管堆上分配對象的母親... 
            Object * pobj = new Object; 
            // 本機堆上分配的標準 string 類... 
            string * pstr = new string;  
            

            當然,這些指針有顯著的不同。例如,在 pobj指向的對象實體在托管堆的壓縮過程中移動時,pobj會被透明地更新。不存在 pobj及其指向?qū)嶓w之間的關(guān)系這樣一個對象跟蹤的概念。整個 C++ 的指針概念并不是機器地址和間接對象引用的鉸接。一個引用類型的句柄封裝了對象的實際虛擬地址以實現(xiàn)運行時垃圾回收;除了在垃圾回收環(huán)境中破壞這個封裝的后果更加嚴重這一點之外,這很大程度上就像私有數(shù)據(jù)成員封裝了類的實現(xiàn)以實現(xiàn)可擴展性和局部化一樣。

            因此,雖然 pobj看起來像一個指針,但是很多指針的常見特性被禁用了,例如指針算術(shù)和類型系統(tǒng)之外的強制類型轉(zhuǎn)換。如果我們使用完全限定語法來生明和分配一個引用托管類型,我們可以使這個區(qū)別更加顯著

            // 好的,現(xiàn)在這些看起來不同了…… 
            Object __gc * pobj = __gc new Object;  
            string * pstr = new string;   
            

            乍一看,指針解決方案很有道理。畢竟,看起來像一個 new 表達式的自然目標,而且兩者都支持淺拷貝。一個問題是指針不是一個類型抽象,而是一個機器表示(以及一個說明如何解釋第一個字節(jié)地址之后內(nèi)存范圍和內(nèi)部組織的標簽類型),而且這不符合軟件運行庫對內(nèi)存的抽象,以及因此推斷出的自動和安全性。這是一個表述不同范型的對象模型之間的歷史問題。

            第二個問題是(比喻警告:即將嘗試一個牽強的比喻 — 所有腸胃不好的人建議暫停閱讀或者跳到下一段)封閉語言設(shè)計的不可避免的弊端就是被限制重新使用既過分簡單又顯著不同的構(gòu)造,導致在沙漠海市蜃樓中程序員的精神的揮散(比喻警告結(jié)束。)

            重用指針語法造成了程序員的認知混亂:您不得不區(qū)分太多的本機和托管指針,而這會干擾代碼流,最好用一個高級的抽象來管理它。換句話說,作為系統(tǒng)程序員,我們有時需要降尊趨貴來壓榨出一點性能,但是我們不會在這個級別久居。

            原版語言設(shè)計的成功在于對現(xiàn)存 C++程序不加修改即可重編譯,并且提供了只需少量工作就可以在新的托管環(huán)境中發(fā)布現(xiàn)存的界面的包裝模式。之后也可以在托管環(huán)境中添加額外的功能,并且,依時間和經(jīng)驗而異,您可以直接將現(xiàn)存應(yīng)用程序的一部分移植到托管環(huán)境。這對一個有現(xiàn)存代碼基和專門技術(shù)基礎(chǔ)的 C++程序員來說是一個偉大的成就。我們不需要為此慚愧。

            但是,在原版語言設(shè)計的實際語法和視野中有顯著的弱點。這不是設(shè)計者的不足,而是其基礎(chǔ)設(shè)計選擇的保守本質(zhì) — 繼續(xù)保持在現(xiàn)存語言中。這來自對托管支持的誤解,就是它不代表一個域抽象,而是一個革命性的編程范型,需要一個類似于 Stroustrup 引入以支持面向?qū)ο蠛推胀ň幊痰恼Z言擴展。這就是修訂版語言設(shè)計所代表的,以及它必要且合理的原因,即使它給忠于原版語言設(shè)計者帶來一些難題+。這即是本指南和轉(zhuǎn)換工具背后的動機。

            修訂版 C++/CLI 語言設(shè)計

            一旦明確了支持 C++ 中的公共語言基礎(chǔ)結(jié)構(gòu)代表一個獨立的編程范型,隨之而來的就是語言必須被擴展,從而為用戶提供一流編的程體驗,以及與 ISO-C++標準的優(yōu)雅的設(shè)計集成,以注重較大 C++ 社區(qū)的感受,并且贏得其委托和輔助。隨之而來的還有,原版語言設(shè)計的昵稱、C++ 的托管擴展,也必須被替換。

            CLI 的最突出特性是引用類型,并且它在現(xiàn)存 C++語言中的集成代表一個概念的證明。一般的標準是什么?我們需要一種方法來表示托管引用類型,既將其分離,又仍感覺它和現(xiàn)存類型系統(tǒng)類似。這使人們意識到,這個普通形式類別很熟悉,但是也注意到它的唯一功能。類似性是 Stroustrup 在 C++ 的原始發(fā)明中引入的引用類型。因此這個普通形式變?yōu)椋?/p>

             Type TypeModToken Id [ = init ]; 
            

            這里 TypeModToken是語言在新的上下文環(huán)境里重用的符號之一(也類似于引用的引入)。

            這在最初爭議十分強烈,并且仍舊使一些用戶感到很困難。我記得一開始兩個最常見的回應(yīng)是 (a) 我可以用一個 typedef 來處理(不住地眨眼),以及 (b) 這真的不怎么壞(后者提醒了我,我的回復是使用左移和右移操作符來在 iostream 庫中進行輸入和輸出)。

            必要的行為特性是它在操作符應(yīng)用到對象的時候展示了對象的語義,這是原版語法無法支持的一點。我喜歡稱它為靈活的引用,思考它和現(xiàn)存 C++ 引用的差異(是的,這里兩個引用的使用 — 一個是托管引用類型,另一個是“這不是一個指針(不住地眨眼)”這里的本機 C++類型 — 是令人遺憾的,很像在我喜歡的一本四人幫(Gangof FourPatterns)的設(shè)計模式書中對模板這個詞的重用。):

            1. 它必須會不引用任何一個對象。當然,本機引用不能直接做到這一點,雖然人們總是告訴我如何將一個引用初始化為 0 的重新解釋的類型強制轉(zhuǎn)換。(常規(guī)的做法是使引用不指向任何對象,從而提供一個顯式的由約定 null對象代表的單體,它也經(jīng)常作為一個函數(shù)參數(shù)的默認參數(shù)。)

            2. 它可能不需要一個初始值,但是可以在生命期的開始不引用任何一個對象。

            3. 它可以被重新賦值為指向另一個對象

            4. 默認情況下,一個實例對另一個實例進行的賦值或者初始化是淺拷貝。

            就像一些人使我想到的,我是從反方向考慮這個問題的。也就是說,我通過區(qū)分它和本機引用的性質(zhì)來引用它,而不是用它作為一個托管引用類型的句柄這個性質(zhì)來識別它。

            我們想將這個類型稱為句柄而不是指針或者引用,因為這兩個術(shù)語有本機方面的累贅。句柄是更適合的名字,因為它是一個封裝的模式 — 一個叫做 John Carolan 的人首先向我介紹了該設(shè)計,以一個可愛的名稱:露齒嬉笑的貓 (CheshireCat),因為被操作對象的實體可以在您不知情的情況下消失。

            在這種情況下,這個令人失望的舉動源自于在垃圾回收器的一次清掃中潛在的引用類型的重新定位。實際上發(fā)生的是,這個重新定位被運行庫透明地跟蹤,而且句柄被更新為正確地指向新位置。這就是它被稱為跟蹤句柄的原因。

            因此,關(guān)于新的跟蹤引用語法,我最后想提及的一點是成員選擇操作符。對我來說,毫無疑問會使用對象語法 (.)。其他人覺得指針語法 (->) 也是同樣顯然的,并且我們從跟蹤引用用途的多個方面進行了討論:

            // 喜好使用指針語法的人 
            T^ p = gcnew T; 
            // 喜好使用對象語法的人 
            T^ c = a + b; 
            

            這樣,就像物理學里面的光一樣,一個跟蹤引用的行為在一些程序上下文中像一個對象,在另一些程序上下文中像一個指針。最后,投入使用的成員選擇操作符是箭頭,就像在原版語言設(shè)計中一樣。

            關(guān)于關(guān)鍵字的總結(jié)性補充

            最后,一個有趣的問題是,為什么Stroustrup在C++語言設(shè)計中添加了類?實際上沒有必要引入它,因為在 C++中 C語言的結(jié)構(gòu)被擴展了,以支持類可以做到的任何事情。我沒有問過 Bjarne 這個問題,所以我在這一點上沒有特別的見識,但是這是一個有趣的問題,而且給定 C++/CLI中添加關(guān)鍵字的數(shù)量,這在某種程度上是相關(guān)的。

            一個可能的回答 — 我稱其為步兵的來福槍(footsoldiershuffle)— 是個爭論:不,類的引入絕對必要。畢竟,兩個關(guān)鍵字之間不僅有默認成員訪問級別的差異,而且派生關(guān)系的訪問級別也不一樣,所以為什么我們不能兩個都要?

            但是慢一點,引入一個新關(guān)鍵字不僅和現(xiàn)存語言不兼容,而且導入了語言樹的一個不同分支(Simula-68),會有冒犯 C語言社區(qū)的風險。其動機真的是默認訪問級別規(guī)則的差異?我不能肯定。

            一方面,語言在類設(shè)計者使用 class關(guān)鍵字將整個實現(xiàn)公開時既沒有阻止也沒有警告。語言本身并無公共和私有訪問的策略,所以很難看到未明確的默認訪問級別許可被重視 — 換句話說,比引入不兼容性的代價還重要。

            類似的,將未標記的基類默認作為私有繼承,看起來在設(shè)計實踐上有些問題。這更加復雜,并且對于繼承的形式更難于理解,因為它沒有展示類型/子類的行為,并且因此破壞了可替代性規(guī)則。它代表了實現(xiàn)(而不是接口)的重用,并且我相信,把私有繼承作為默認是個錯誤。

            當然,我不能公開宣布這一點,因為在語言市場中,從來不應(yīng)該承認產(chǎn)品中的一點點問題,因為這會為迅速抓住任何競爭優(yōu)勢搶占市場分額的敵人提供彈藥。嘲笑在知識分子的小圈子里特別盛行。或者,更恰當?shù)卣f,一個人直到新的改進產(chǎn)品準備鋪開的時候再承認缺陷。

            引入 class不兼容性還可能有什么其他原因?C語言的結(jié)構(gòu)概念是一個抽象的數(shù)據(jù)類型。C++語言的類概念(當然,這不是源自于 C++)是數(shù)據(jù)抽象,以及隨之而來的封裝和接口約定的思想。抽象數(shù)據(jù)類型是與地址相關(guān)的鄰近的數(shù)據(jù)塊 — 指向它、數(shù)據(jù)轉(zhuǎn)換、分隔、快速移動。數(shù)據(jù)抽象是有生命期和行為的實體。這是為了教育學上的重要性,因為用詞會使語言大不一樣 — 至少在一個語言中。這是修訂版設(shè)計銘記在心的另一個教訓。

            為什么 C++沒有完全移除結(jié)構(gòu)?保留一個并引入另一個并不優(yōu)雅,而且這樣字面上最小化了他們之間的差異。但是有其它選擇嗎?Struct 關(guān)鍵字不得不被保留,因為 C++必須盡可能保留和 C的向下兼容;否則,不僅它會在現(xiàn)存程序員中更不受歡迎,而且可能不會被允許發(fā)布(但是這是另一個時間、另一個地點的另一個故事了)

            為什么結(jié)構(gòu)的訪問級別默認是公有的?因為如果不這樣,現(xiàn)存的 C程序不會編譯通過。這在實踐上會是一場災難,雖然程序員很可能從來沒在語言設(shè)計高級守則(Advanced Principles of Language Design)中聽說過它。語言中可能有一個強制接受的過程,強制接受一個策略,從而使用結(jié)構(gòu)保證了一個公有實現(xiàn),反之,使用類保證了一個私有實現(xiàn)和公有接口,但是這個策略并不用于實踐用途,所以會有點過于珍貴。

            實際上,在貝爾實驗室的 cfront1.0語言編譯器的發(fā)布測試中,有一個語言律師之間的小爭論:前置聲明和后續(xù)定義(或者任何這樣的組合)是否必須繼續(xù)使用這個或者其他關(guān)鍵字,或者可以被互相替換來使用。當然,如果結(jié)構(gòu)有任何實際的重要性,這個爭論不會發(fā)生。

            致謝

            我想在這里感謝 Visual C++ 團隊的很多成員,他們不斷幫助和指引我理解從原版 C++托管擴展到修訂的 C++/CLI語言設(shè)計移植相關(guān)的問題。特別感謝 Arjun Bijanki 和 Artur Laksberg,他們兩個容忍了我的很大困惑。也感謝 Brandon Bray、Jonathan Caves、Siva Challa、Tanveer Gani、Mark Hall、Mahesh Hariharan、Jeff Peil、Andy Rich、Alvin Chardon 和 Herb Sutter。他們都提供了大量的幫助和反饋。本文頌揚了他們的專業(yè)精神。

            相關(guān)書籍

            STL Tutorial and Reference Guide ,David Musser、Gillmer Derge 和 Atul Saini 著,Addison-Wesley,2001 年

            C++ Standard Library ,Nicolai Josuttis 著,Addison-Wesley,1999 年

            C++ Primer ,Stanley Lippman 和 Josee Lajoie 著,Addison-Wesley,1998 年

            關(guān)于作者

            Stanley Lippman 是微軟 Visual C++ 團隊的一個架構(gòu)師,曾在 1984 年于貝爾實驗室和 C++ 發(fā)明者 Bjarne Stroustrup 一起工作。其間曾工作于華特·迪士尼和夢工場的特色動畫公司,也是影片《狂想曲兩千》(Fantasia 2000)的軟件技術(shù)指導。

            posted on 2008-10-30 17:08 BeyondCN 閱讀(1367) 評論(0)  編輯 收藏 引用 所屬分類: .NET
            99久久99久久| 思思久久99热免费精品6| 久久夜色精品国产噜噜噜亚洲AV| 久久香蕉超碰97国产精品| 97久久综合精品久久久综合| 国内精品久久久久久久亚洲| 久久99久国产麻精品66| 99久久综合国产精品二区| 久久久久人妻一区二区三区 | 97精品伊人久久大香线蕉app| 99久久精品免费看国产一区二区三区 | 一本久久综合亚洲鲁鲁五月天| 久久综合国产乱子伦精品免费| 久久99国产精一区二区三区| 亚洲国产精品成人久久蜜臀 | 99久久国产热无码精品免费| 国产一区二区三精品久久久无广告| 久久www免费人成看片| 久久乐国产精品亚洲综合| 97热久久免费频精品99| 波多野结衣久久| 久久综合给合综合久久| 青青青国产成人久久111网站| 亚洲av成人无码久久精品| 欧美成人免费观看久久| 国产成人精品久久综合 | 久久天天躁狠狠躁夜夜avapp| 国产精品激情综合久久| 久久精品国产亚洲网站| 99久久99这里只有免费的精品| 久久国产精品无码HDAV| 日韩精品久久久久久免费| 日韩久久久久久中文人妻| 亚洲午夜久久久影院| 午夜天堂av天堂久久久| 久久综合狠狠综合久久| 亚洲AV无码1区2区久久| 欧美黑人激情性久久| 成人免费网站久久久| 久久精品国产亚洲沈樵| 伊人热人久久中文字幕|