青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品

posts - 17,  comments - 2,  trackbacks - 0
轉(zhuǎn)換指南: 將程序從托管擴(kuò)展 C++ 遷移到 C++/CLI
發(fā)布日期 : 5/31/2005 | 更新日期 : 5/31/2005

Stanley B. Lippman 
Microsoft Corporation

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

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

本頁內(nèi)容

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

簡(jiǎn)介

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

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

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

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

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

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

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

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

類似的,我們很關(guān)心最小化這些新的關(guān)鍵字的對(duì)現(xiàn)有代碼可能造成的沖擊。這是用與上下文相關(guān)和由空格分隔的關(guān)鍵字來解決的。在我們著眼于實(shí)際語言語法的修訂之前,讓我們?cè)囋嚫闱宄@兩個(gè)特別關(guān)鍵字的特點(diǎn)。

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

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

   #ifndef __cplusplus_cli 
   #define value 

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

   #ifndef __cplusplus_cli 
   #define value class class 

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

2. 托管類型

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

2.1 聲明一個(gè)托管類類型

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

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

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

例如,下列類聲明對(duì)

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

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

2.1.1 指定一個(gè)類為抽象類型

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

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

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

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

當(dāng)然,語義沒有變化。

2.1.2 指定一個(gè)類為密封類型

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

   public __gc __sealed class String {}; 

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

   public ref class String sealed {}; 

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

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

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

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

2.1.3 CLI 繼承 指定基類

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

// V1:錯(cuò)誤:默認(rèn)為私有派生 
__gc class My : File{}; 

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

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

2.2 一個(gè) CLI 的引用類對(duì)象的聲明

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

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è)計(jì)中,引用類類型的對(duì)象用一個(gè)新的聲明性符號(hào)(^)聲明,正式的表述為跟蹤句柄,不正式的表述為帽子。(跟蹤這個(gè)形容詞強(qiáng)調(diào)了引用類型對(duì)象位于 CLI堆中,因此可以透明地在垃圾回收堆的壓縮過程中移動(dòng)它的位置。一個(gè)跟蹤句柄在運(yùn)行時(shí)被透明地更新。兩個(gè)類似的概念:(a)跟蹤引用(%) 和 (b)內(nèi)部指針(interior_ptr<>),在第4.4.3節(jié)討論。

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

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

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

對(duì)一個(gè)跟蹤句柄使用 __gc修飾符是不必要的,而且是不被支持的。對(duì)象本身的用法并未變化,它仍舊通過指針成員選擇操作符 (->) 訪問成員。例如,以下是上面的 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 堆上動(dòng)態(tài)分配對(duì)象

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

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

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

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

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

以下是前面一節(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); 
      // ... 
} 

以下是用修訂版語法重寫的同樣的初始化過程,注意引用類型是一個(gè) gcnew表達(dá)式的目標(biāo)時(shí)不需要“帽子”。

   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 無對(duì)象的跟蹤引用

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

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

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

//導(dǎo)致 0 和 1 的隱式裝箱 
Object ^ obj = 0; 
Object ^ obj2 = 1; 

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

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

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

__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ù)中將這兩個(gè)成員初始化為 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進(jìn)行比較的測(cè)試也必須改為和 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; 
   } 

而以下是修訂版語法。將每個(gè) 0實(shí)例轉(zhuǎn)換為 nullptr (轉(zhuǎn)換工具有助于這個(gè)轉(zhuǎn)換,進(jìn)行許多自動(dòng)處理 — 如果不是全部出現(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)化成任何跟蹤句柄類型或者指針,但是不能提升為一個(gè)整數(shù)類型。例如,在如下初始化集合中,nullptr只在開頭兩個(gè)初始值中有效。

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

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

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

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

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

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

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

2.3 CLI 數(shù)組的聲明

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

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

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

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

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

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

在 V2中,這個(gè)聲明閱讀和分析起來簡(jiǎn)單多了。例如:

// 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中被大大簡(jiǎn)化了(注意因?yàn)樾抻啺嬲Z言設(shè)計(jì)中的裝箱是隱式的,__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}; 
// ... 
} 

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

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

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

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

在修訂版語言中,gcnew表達(dá)式后面可以跟一個(gè)顯式的初始化列表,這在 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è)計(jì)中沒有變化。但是,類析構(gòu)函數(shù)的語義有可觀的變化。怎樣和為什么變化(以及這會(huì)對(duì)現(xiàn)存 V1代碼的轉(zhuǎn)換造成怎樣的影響)是本節(jié)的主題。這可能是本文中最復(fù)雜的一節(jié),所以我們慢慢來講。這也可能是兩個(gè)語言版本之間最重要的編程級(jí)別的修改,所以需要以循序漸進(jìn)的方式來進(jìn)行學(xué)習(xí)。

2.4.1 不確定的終止

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

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

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

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

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

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

  1. 用戶編寫的析構(gòu)函數(shù)被內(nèi)部重命名為 Finalize()。如果類有一個(gè)基類(記住,在 CLI對(duì)象模型中只支持單繼承),編譯器在用戶的代碼之后插入一個(gè)對(duì)其終結(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è)析構(gòu)函數(shù)都被重命名為 Finalize()B  Finalize()在調(diào)用 WriteLine()之后加入一個(gè) A Finalize()方法的調(diào)用。這些就是垃圾回收器在終止過程中默認(rèn)調(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è)虛析構(gòu)函數(shù)。這個(gè)析構(gòu)函數(shù)就是我們的 V1用戶程序直接調(diào)用或者通過 delete表達(dá)式的應(yīng)用程序調(diào)用的。它永遠(yuǎn)不會(huì)被垃圾回收器調(diào)用。

    這個(gè)產(chǎn)生的析構(gòu)函數(shù)里面有什么內(nèi)容呢?是兩個(gè)語句。一個(gè)是調(diào)用GC::SuppressFinalize()以確保沒有對(duì) Finalize()方法的進(jìn)一步調(diào)用。另一個(gè)是實(shí)際上的 Finalize()調(diào)用。回憶一下,這表達(dá)了用戶提供的這個(gè)類的析構(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(); 
          } 
    }; 
    

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

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

在修訂版語言設(shè)計(jì)中,析構(gòu)函數(shù)被內(nèi)部重命名為 Dispose()方法,并且引用類自動(dòng)擴(kuò)展以實(shí)現(xiàn) IDisposable接口。換句話說,在 V2中,這對(duì)類按如下所示進(jìn)行轉(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 中,當(dāng)析構(gòu)函數(shù)被顯式調(diào)用時(shí),或者對(duì)跟蹤句柄應(yīng)用 delete時(shí),底層的 Dispose()方法都會(huì)自動(dòng)被調(diào)用。如果這是一個(gè)派生類,一個(gè)對(duì)基類的 Dispose()方法的調(diào)用會(huì)被插入到生成方法的末尾。

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

2.4.4 聲明一個(gè)引用對(duì)象

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

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

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

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

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

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

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

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

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

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

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

! 前綴表示引入類析構(gòu)函數(shù)的類似符號(hào) (~),也就是說,兩種后生命周期的方法名都是在類名前加一個(gè)符號(hào)前綴。如果派生類中有一個(gè)合成的 Finalize()方法,那么在其末尾會(huì)插入一個(gè)基類的Finalize()方法的調(diào)用。如果析構(gòu)函數(shù)被顯式地調(diào)用,那么終止器會(huì)被抑制。這個(gè)轉(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è)引用類包含一個(gè)特別的析構(gòu)函數(shù),一個(gè) V1程序在 V2 編譯器下的運(yùn)行時(shí)行為被靜默地修改了。需要的轉(zhuǎn)換算法如下所示:

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

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

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

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

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

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

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

3.1 屬性聲明

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

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; } 
}; 

這使人感到迷惑,因?yàn)閷傩韵嚓P(guān)的函數(shù)被展開了,并且需要用戶從語法上統(tǒng)一相關(guān)的 set  get。而且它在語法上過于冗長(zhǎng),并且感覺上不甚優(yōu)雅。在修訂版語言設(shè)計(jì)中,這個(gè)聲明更類似于 C# — property 關(guān)鍵字后接屬性的類型以及屬性的原名。set 存取get 存取方法放在屬性名之后的一段中。注意,與 C# 不同,存取方法的符號(hào)被指出。例如,以下是上面的代碼轉(zhuǎn)換為新語言設(shè)計(jì)后的結(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)為不同的訪問級(jí)別 — 例如一個(gè)公有的 get和一個(gè)私有的或者保護(hù)的 set,那么可以指定一個(gè)顯式的訪問標(biāo)志。默認(rèn)情況下,屬性的訪問級(jí)別反映了它的封閉訪問級(jí)別。例如,在上面的 Vector定義中,get set方法都是公有的。為了讓 set方法成為保護(hù)或者私有的,必須如下修改定義:

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

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

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

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

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

3.2 屬性索引聲明

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

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ù)來指定一個(gè)二維或者一維的索引,從而區(qū)分索引器。在修訂版語法中,索引器由名字后面的方括號(hào) ([,]) 區(qū)分,并且表示每個(gè)索引的數(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 ); 
   } 
}; 

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

public ref class Matrix
{
private:
   array<float, 2>^ mat;
public:
      //OK,現(xiàn)在有類級(jí)別的索引器了
       //
       // Matrix mat ...
       //     mat[ 0, 0 ] = 1; 
       //
       // 調(diào)用默認(rèn)索引器的 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 );
   }
};

在修訂版語法中,當(dāng)指定了 default索引屬性時(shí),下面兩個(gè)名字被保留:get_Itemset_Item。這是因?yàn)樗鼈兪?#160;default索引屬性產(chǎn)生的底層名稱。

注意,簡(jiǎn)單索引語法與簡(jiǎn)單屬性語法截然不同。

3.3 委托和事件

聲明一個(gè)委托和普通事件僅有的變化是移除了雙下劃線,如下面的示例所述。在去掉了之后,這個(gè)更改被認(rèn)為是完全沒有爭(zhēng)議的。換句話說,沒有人支持保持雙下劃線,所有人現(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中更為明顯,因?yàn)橛忻弊?(^) 的存在。除了普通形式之外,事件支持一個(gè)顯式的聲明語法,用戶顯式指定事件關(guān)聯(lián)的 add()raise()、和 remove()方法。(只有 add()和 remove()方法是必須的;raise()方法是可選的)。

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

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

// 修訂版 V2 語言設(shè)計(jì) 
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è)計(jì)方面,人們因?yàn)檎Z法的簡(jiǎn)單枯燥而傾向于忽視它,但是如果對(duì)語言的用戶體驗(yàn)有很大的潛移默化的影響,那么它實(shí)際上很有意義。一個(gè)令人迷惑的、不優(yōu)雅的語法可能增加開發(fā)過程的風(fēng)險(xiǎn),很大程度上就像一個(gè)臟的或者不清晰的擋風(fēng)玻璃增加開車的風(fēng)險(xiǎn)一樣。在修訂版語言設(shè)計(jì)中,我們努力使語法像一塊高度磨光的新安裝的擋風(fēng)玻璃一樣透明。

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

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

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

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

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

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

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

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

可以改寫為

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

3.5 操作符重載

原版語言設(shè)計(jì)最驚人之處可能是它對(duì)于操作符重載的支持 — 或者更恰當(dāng)?shù)卣f,是有效的缺乏支持。舉例來說,在一個(gè)引用類型的聲明中,不是使用內(nèi)建的 operator+語法,而是必須顯式編寫出操作符的底層內(nèi)部名稱 — 在本例中是 op_Addition。但更加麻煩的是,操作符的調(diào)用必須通過該名稱來顯式觸發(fā),這樣就妨礙了操作符重載的兩個(gè)主要好處:(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è)計(jì)中必須編寫 op_Implicit來指定一個(gè)轉(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 );  
}; 

這就是說,給定一個(gè)整數(shù),將這個(gè)整數(shù)轉(zhuǎn)換為 MyDouble的算法是通過op_Implicit操作符實(shí)現(xiàn)的。進(jìn)一步說,這個(gè)轉(zhuǎn)換將被編譯器隱式執(zhí)行。類似的,給定一個(gè) MyDouble對(duì)象,兩個(gè)op_Explicit操作符分別提供了以下兩種算法:將對(duì)象轉(zhuǎn)換為整型或者托管字符串實(shí)體。但是,編譯器不會(huì)執(zhí)行這個(gè)轉(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 );  
}; 

除了每個(gè)成員都有的顯式公有 訪問標(biāo)志看起來很古怪,C#代碼看起來比 C++的托管擴(kuò)展更加像 C++。所以我們不得不修復(fù)這個(gè)問題。但是我們?cè)趺床拍茏龅剑?/p>

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

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

這樣,將轉(zhuǎn)換操作符的隱式/顯式行為進(jìn)行關(guān)聯(lián)(以及將一組轉(zhuǎn)換封裝到一組聲明)看起來是原始 C++ 對(duì)轉(zhuǎn)換操作符支持的改進(jìn),這個(gè)支持自從 1988 年 Robert Murray 發(fā)布了關(guān)于 UsenixC++的標(biāo)題為 Building Well-Behaved Type Relationships in C++的講話之后,已經(jīng)成為一個(gè)公開的警世篇,講話最終產(chǎn)生了 explicit 關(guān)鍵字。修訂版 V2語言對(duì)轉(zhuǎn)換操作符的支持如下所示,比 C# 的支持稍微簡(jiǎn)略一點(diǎn),因?yàn)椴僮鞣哪J(rèn)行為支持隱式轉(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的另一個(gè)變化是,V2中的單參數(shù)構(gòu)造函數(shù)以聲明為 explicit 的方式處理。這意味著為了觸發(fā)它的調(diào)用,需要一個(gè)顯式的轉(zhuǎn)換。但是要注意,如果一個(gè)顯式的轉(zhuǎn)換操作符已經(jīng)定義,那么是它而不是單參數(shù)構(gòu)造函數(shù)會(huì)被調(diào)用。

3.7 接口成員的顯式重寫

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

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

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

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

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

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

3.8 私有虛函數(shù)

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

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

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

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

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

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

雖然 static const整型成員仍舊被支持,但是它們的 linkage 屬性被修改了。以前的 linkage 屬性現(xiàn)在通過一個(gè) 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運(yùn)行庫(kù)視為一個(gè)常量。

.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枚舉類型和值類類型,同時(shí)研究裝箱和對(duì) CLI堆上裝箱實(shí)例的訪問,以及考慮內(nèi)部和釘住指針。這個(gè)領(lǐng)域的語言變化范圍很廣。

4.1 CLI 枚舉類型

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

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

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

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

修訂版語言設(shè)計(jì)中的枚舉對(duì) 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 中沒有這樣的對(duì)應(yīng)。這只會(huì)導(dǎo)致編譯時(shí)錯(cuò)誤。

    __value enum status; // V1: 正確 
    enum class status;   // V2: 錯(cuò)誤 
    
  2. 在內(nèi)建算術(shù)類型和對(duì)象類層次結(jié)構(gòu)之間進(jìn)行重載解析的次序在 V2和 V1中被顛倒了!一個(gè)副作用是,托管枚舉在 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? 
} 

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

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

   void f( int ); 

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

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

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

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

4.1.2 CLI 枚舉類型的范圍

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

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

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

// 原版語言設(shè)計(jì)支持弱插入 
__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) 
   } 
}; 

三個(gè)枚舉數(shù)名稱的未限定使用 ((1)(2)(3)) 都需要在轉(zhuǎn)換為修訂版語言語法時(shí)被限定,從而讓源代碼通過編譯。以下是原始源代碼的正確轉(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) 
  } 
}; 

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

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

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

4.2 隱式裝箱

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

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

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

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

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

以下是一個(gè)代碼實(shí)例,完成一些簡(jiǎn)單的功能 — 在本例中,顯式裝箱很大程度上是無用的語法負(fù)擔(dān)。

   // 原版語言設(shè)計(jì)需要顯式 __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)) ); 

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

   // 修訂版語言進(jìn)行隱式裝箱 
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)的一個(gè)特性。值類型直接包含其狀態(tài),而引用類型有雙重含義:命名實(shí)體是一個(gè)句柄,這個(gè)句柄指向托管堆上分配的一個(gè)非命名對(duì)象。舉例來說,任何從類型到對(duì)象的初始化或者賦值,都需要類型放在 CLI 堆中(圖像裝箱發(fā)生的位置)首先分配相關(guān)的內(nèi)存,然后復(fù)制類型的狀態(tài),最后返回這個(gè)匿名 / 引用的組合。因此,用 C# 編寫如下代碼時(shí),

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

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

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

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

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

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

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的三個(gè)調(diào)用生成的底層代碼顯示了訪問裝箱值類型值的不同代價(jià)(感謝Yves Dolce指出這些差異),這里黑體的行顯示了與每個(gè)調(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()的需要(當(dāng)然,這是用前面提到的對(duì) result的裝箱來初始化 br),所以除非真正使用 br,否則我們不會(huì)真正有所收獲。

在修訂版語言語法中,在保持裝箱類型的優(yōu)點(diǎn)的同時(shí),對(duì)它的支持也變得更加優(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ī)范中使用的一個(gè)規(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)是一個(gè)規(guī)范的值對(duì)象,并且它是相當(dāng)容易理解的,除非有人試圖調(diào)用一個(gè)繼承虛方法,例如 ToString()。例如,

v.ToString(); // 錯(cuò)誤! 

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

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

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

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

   v.ToString(); // V2 

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

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

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

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

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

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

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

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

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

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

4.4.3 內(nèi)部指針

格式(2)和 (3)幾乎可以解決任何問題(即托管和本機(jī))。因此,舉例來說,在原版語言設(shè)計(jì)中以下內(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;         //指向棧上的一個(gè)值類型 
pv = __nogc new V;  //指向本機(jī)堆上的一個(gè)值類型 
pv = pvgc;          // 我們不確定這指向什么位置 
pv = pvbx;        // 指向托管堆上的裝箱值類型 
pv = &r->vr;        //指向托管堆上一個(gè)引用類型中的值類型的內(nèi)部指針 

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

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

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

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

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

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

原版語言設(shè)計(jì)中的下列聲明在修訂版語言設(shè)計(jì)中都對(duì)應(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)建類型不被認(rèn)為是托管類型,雖然它們確實(shí)作為 System命名空間內(nèi)的類型的別名。因此,原版和修訂版語言的以下對(duì)應(yīng)是正確的:

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

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

4.4.4 釘住指針

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

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

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

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

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

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

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

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

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

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

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

5. 語言變化概要

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

5.1 字符串

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

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

兩個(gè)初始化之間的性能開銷差別并不小,如下面通過 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 

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

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

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

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

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

這里發(fā)生了什么?為什么有區(qū)別?因?yàn)槌绦蛑写嬖诓恢挂粋€(gè)以 f 為名稱的實(shí)例,所以需要將函數(shù)重載解析算法應(yīng)用于調(diào)用。正式的函數(shù)重載解析包含以下三個(gè)步驟:

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

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

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

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

這導(dǎo)致我們思考以下兩個(gè)問題:

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

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

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

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

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

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

  • 標(biāo)準(zhǔ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)換是錯(cuò)誤之前的最后一個(gè)出口(警告:形式參數(shù)中的位置可能包含一個(gè)參數(shù)數(shù)組或者省略號(hào))。

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

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

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

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

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

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

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

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

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

在原版語言設(shè)計(jì)中,省略號(hào)的解析優(yōu)先于屬性,這是有道理的,因?yàn)閷傩圆皇钦Z言的正式部分。然而在 V2中,現(xiàn)在語言直接支持參數(shù)數(shù)組,所以它優(yōu)先于省略號(hào),因?yàn)樗歉鼜?qiáng)類型的。因此,在原版語言中,調(diào)用

My( 1, 2 );     

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

5.3 typeof 改為 T::typeid

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

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

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

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

5.4 強(qiáng)制轉(zhuǎn)換符號(hào)和 safe_cast<> 簡(jiǎn)介

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

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

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

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

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

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

舉例來說,一個(gè)電腦成像 (CGI) 應(yīng)用程序域中一個(gè)輕量級(jí)的的層次結(jié)構(gòu),會(huì)具有一些諸如 colorintensitypositiononoff等等的共同屬性。可以在某個(gè)圖像中撒下幾束光,并且通過通用接口控制它們,而不用擔(dān)心光到底是聚光、平行光、全向光(如太陽光),還是通過擋光板的光。在這種情況下,向下轉(zhuǎn)換為一個(gè)特定的光類型來實(shí)現(xiàn)其虛接口是不必要的,因?yàn)樗械姆绞蕉家粯樱允遣幻髦堑摹5牵谏a(chǎn)環(huán)境中,情況不總是一樣的;很多情況下,考慮的是速度;程序員可能會(huì)選擇向下轉(zhuǎn)換,然后顯式調(diào)用每個(gè)方法,如果這樣,調(diào)用的內(nèi)聯(lián)直接執(zhí)行會(huì)替代通過虛函數(shù)機(jī)制執(zhí)行。

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

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

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

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

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

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

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

關(guān)于類型安全的向下轉(zhuǎn)換的下一個(gè)問題是它的語法。因?yàn)樗且粋€(gè)強(qiáng)制轉(zhuǎn)換,ISO-C++協(xié)會(huì)的原意是使用未裝飾的強(qiáng)制轉(zhuǎn)換語法,因此編寫如下示例代碼:

   spot = ( SpotLight* ) plight; 

但是這被委員會(huì)否決了,因?yàn)檫@不允許用戶控制強(qiáng)制轉(zhuǎn)換的代價(jià)。如果動(dòng)態(tài)類型安全的向下轉(zhuǎn)換具有前面的不安全但是是靜態(tài)的標(biāo)記,那么它將成為一個(gè)替代方案,而且用戶無法在不必要或者代價(jià)太大時(shí)降低運(yùn)行庫(kù)的開銷。

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

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

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

在 ISO-C++中,dynamic_cast在應(yīng)用到一個(gè)不合適的指針類型時(shí)返回 0,并且在應(yīng)用到一個(gè)引用類型時(shí)引發(fā)一個(gè) std::bad_cast異常。在原版語言設(shè)計(jì)中,將 dynamic_cast應(yīng)用到一個(gè)托管引用類型(因?yàn)樗闹羔槺磉_(dá)方法)總是返回 0__try_cast<type>作為一個(gè)引發(fā) dynamic_cast變體的異常的類似被引入,但是它在強(qiáng)制轉(zhuǎn)換失敗時(shí)引發(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)域,限制程序員以代碼不可驗(yàn)證的方式在類型間進(jìn)行轉(zhuǎn)換的能力,從而允許可驗(yàn)證代碼是很重要的。這是 C++/CLI 代表的動(dòng)態(tài)編程范型的一個(gè)關(guān)鍵部分。由于這個(gè)原因,舊風(fēng)格類型轉(zhuǎn)換的實(shí)例作為運(yùn)行庫(kù)轉(zhuǎn)換被內(nèi)部重新轉(zhuǎn)換,所以,舉例來說:

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

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

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

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

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

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

附錄:推動(dòng)修訂版語言設(shè)計(jì)

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

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

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

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

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

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

T t;  

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

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

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

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

   Base b = *new Derived; 

那么 b并不指向一個(gè)位于本機(jī)堆上的 Derived對(duì)象。值 b沒有和 new 表達(dá)式分配的Derived對(duì)象關(guān)聯(lián),而 Derived對(duì)象的 Base部分被截?cái)啵⑶野次粡?fù)制到獨(dú)立的基于棧的實(shí)例 b。這在 CLI對(duì)象模型中實(shí)際上沒有對(duì)應(yīng)的描述。

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

指針和 C++對(duì)象模型一致。在

T *pt = 0;  

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

引用為看起來有些復(fù)雜的指針詞匯提供了一種簡(jiǎn)單的語法,同時(shí)保持其效率。

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

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

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

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

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

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

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

C++設(shè)計(jì)的原版托管擴(kuò)展

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

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

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

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

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

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

  2. 以庫(kù)的方式添加域抽象支持(考慮將標(biāo)準(zhǔn)模板庫(kù)作為可能的模型)。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// 所有在托管堆上分配對(duì)象的母親... 
Object * pobj = new Object; 
// 本機(jī)堆上分配的標(biāo)準(zhǔn) string 類... 
string * pstr = new string;  

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

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

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

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

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

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

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

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

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

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

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

 Type TypeModToken Id [ = init ]; 

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

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

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

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

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

  3. 它可以被重新賦值為指向另一個(gè)對(duì)象

  4. 默認(rèn)情況下,一個(gè)實(shí)例對(duì)另一個(gè)實(shí)例進(jìn)行的賦值或者初始化是淺拷貝。

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

我們想將這個(gè)類型稱為句柄而不是指針或者引用,因?yàn)檫@兩個(gè)術(shù)語有本機(jī)方面的累贅。句柄是更適合的名字,因?yàn)樗且粋€(gè)封裝的模式 — 一個(gè)叫做 John Carolan 的人首先向我介紹了該設(shè)計(jì),以一個(gè)可愛的名稱:露齒嬉笑的貓 (CheshireCat),因?yàn)楸徊僮?strong>對(duì)象的實(shí)體可以在您不知情的情況下消失。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

致謝

我想在這里感謝 Visual C++ 團(tuán)隊(duì)的很多成員,他們不斷幫助和指引我理解從原版 C++托管擴(kuò)展到修訂的 C++/CLI語言設(shè)計(jì)移植相關(guān)的問題。特別感謝 Arjun Bijanki 和 Artur Laksberg,他們兩個(gè)容忍了我的很大困惑。也感謝 Brandon Bray、Jonathan Caves、Siva Challa、Tanveer Gani、Mark Hall、Mahesh Hariharan、Jeff Peil、Andy Rich、Alvin Chardon 和 Herb Sutter。他們都提供了大量的幫助和反饋。本文頌揚(yáng)了他們的專業(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++ 團(tuán)隊(duì)的一個(gè)架構(gòu)師,曾在 1984 年于貝爾實(shí)驗(yàn)室和 C++ 發(fā)明者 Bjarne Stroustrup 一起工作。其間曾工作于華特·迪士尼和夢(mèng)工場(chǎng)的特色動(dòng)畫公司,也是影片《狂想曲兩千》(Fantasia 2000)的軟件技術(shù)指導(dǎo)。

posted on 2008-10-30 17:08 BeyondCN 閱讀(1392) 評(píng)論(0)  編輯 收藏 引用 所屬分類: .NET
青青草原综合久久大伊人导航_色综合久久天天综合_日日噜噜夜夜狠狠久久丁香五月_热久久这里只有精品
  • <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>
            美女精品一区| 亚洲视频1区| 欧美精品一区在线播放| 久久国产精品99国产| 欧美一区二区三区日韩视频| 午夜精品久久99蜜桃的功能介绍| 亚洲一本大道在线| 亚洲欧美综合一区| 久久综合狠狠综合久久综合88| 久久综合中文字幕| 欧美日韩小视频| 国产精品亚洲第一区在线暖暖韩国| 国产乱码精品1区2区3区| 国内成人自拍视频| 欧美国产免费| 亚洲人成7777| 一本色道久久综合亚洲精品婷婷| 日韩亚洲一区在线播放| 亚洲欧美日韩在线不卡| 久久女同精品一区二区| 亚洲二区免费| 亚洲视频一区在线| 欧美在线观看一区二区三区| 久久久久.com| 国产精品久久久久久久久借妻| 国产日韩在线看片| 亚洲人成在线影院| 欧美在线观看一区| 欧美激情精品久久久久| 日韩天堂在线观看| 久久男人av资源网站| 欧美手机在线| 亚洲精品乱码久久久久久蜜桃91| 亚洲一区在线看| 欧美超级免费视 在线| 亚洲午夜久久久久久久久电影网| 久久精选视频| 国产精品视频专区| 日韩亚洲欧美中文三级| 蜜桃精品一区二区三区| 妖精成人www高清在线观看| 久久视频国产精品免费视频在线 | 久久久91精品国产一区二区三区 | 国模精品娜娜一二三区| 亚洲乱码国产乱码精品精| 久久夜色精品国产| 亚洲一卡久久| 欧美日韩黄色大片| 在线观看av不卡| 久久er99精品| 亚洲欧美另类在线| 国产精品捆绑调教| 亚洲一区二区免费| 亚洲日韩欧美视频一区| 另类激情亚洲| 亚洲精美视频| 欧美v亚洲v综合ⅴ国产v| 欧美一区二区三区精品电影| 欧美日韩三区四区| aa成人免费视频| 亚洲精品美女久久7777777| 欧美成人一区二区三区片免费| 国内精品久久久久久久97牛牛| 欧美在线观看一二区| 香蕉久久夜色精品| 国产亚洲欧美日韩美女| 久久久国产精品一区| 欧美伊人久久久久久午夜久久久久| 亚洲福利视频二区| 亚洲乱码国产乱码精品精可以看 | av成人老司机| 欧美喷水视频| 一本色道久久综合狠狠躁的推荐| 亚洲国产欧美在线| 欧美日韩国内| 篠田优中文在线播放第一区| 香蕉免费一区二区三区在线观看 | 欧美黄在线观看| 在线视频你懂得一区二区三区| 一区二区高清在线| 国产亚洲精品久| 欧美电影在线观看| 欧美精品综合| 欧美一区二区三区播放老司机| 欧美专区日韩视频| 亚洲青色在线| 亚洲天堂第二页| 精品电影一区| 亚洲欧洲午夜| 国产欧美精品日韩精品| 免费成人av在线| 欧美日韩一区二区三区在线视频| 欧美一级理论性理论a| 久久色在线播放| 亚洲一二三区在线观看| 久久久久久午夜| 亚洲深夜福利在线| 久久精品在线| 亚洲影院色在线观看免费| 久久福利精品| 亚洲尤物在线视频观看| 久久久久久久久岛国免费| 亚洲一区二区三区在线| 能在线观看的日韩av| 欧美一区二区三区视频在线 | 欧美日韩不卡一区| 久久精品国产久精国产爱| 欧美激情1区| 久久婷婷麻豆| 欧美视频在线观看免费| 欧美成人国产一区二区| 国产日韩精品一区二区| 亚洲日本激情| 在线欧美日韩精品| 欧美亚洲日本网站| 亚洲欧美在线网| 欧美激情视频网站| 麻豆久久婷婷| 国产日韩一区二区三区| 在线中文字幕一区| 在线综合+亚洲+欧美中文字幕| 久久久久久夜| 久久久久久自在自线| 国产精品丝袜久久久久久app| 欧美在线免费观看| 亚洲伦理中文字幕| 亚洲黄色成人网| 久久久噜噜噜久久| 久久精品国产欧美激情| 国产精品每日更新| 一区二区三区欧美成人| 一区二区三区日韩精品| 欧美电影免费观看大全| 欧美激情精品久久久久久蜜臀 | 国产精品99免费看 | 国产精品亚洲视频| 亚洲一区二区成人在线观看| 亚洲图色在线| 欧美日韩综合一区| 亚洲素人在线| 欧美亚洲综合另类| 国产亚洲欧洲| 久久久久久久一区二区三区| 牛人盗摄一区二区三区视频| 在线日本成人| 欧美jizz19性欧美| 最新日韩精品| 亚洲视频在线一区观看| 欧美色道久久88综合亚洲精品| 99精品免费视频| 欧美在线免费视频| 狠狠色狠狠色综合日日tαg| 久久精品人人| 亚洲国产精品ⅴa在线观看 | 国产精品麻豆va在线播放| 亚洲一区免费观看| 久久久午夜视频| 亚洲国产网站| 欧美性做爰毛片| 欧美一区二区观看视频| 开元免费观看欧美电视剧网站| 亚洲国产精品专区久久| 欧美绝品在线观看成人午夜影视| 日韩香蕉视频| 欧美在线观看网址综合| 亚洲高清毛片| 国产精品卡一卡二| 麻豆精品传媒视频| 在线综合亚洲欧美在线视频| 久久青草久久| 亚洲视频视频在线| 伊人久久婷婷色综合98网| 欧美另类极品videosbest最新版本| 亚洲深爱激情| 欧美成人国产va精品日本一级| 在线综合亚洲欧美在线视频| 国产亚洲精品bt天堂精选| 欧美高清在线视频| 午夜综合激情| 日韩视频国产视频| 久久综合狠狠综合久久综青草| 一本色道久久综合亚洲精品婷婷| 国产亚洲欧美日韩美女| 欧美日韩精品一本二本三本| 欧美一级网站| 一本久道久久综合中文字幕| 久久亚洲综合色| 午夜精品三级视频福利| 亚洲毛片一区| 国产酒店精品激情| 欧美一级网站| 99国产精品久久久久老师| 国产亚洲亚洲| 国产精品激情av在线播放| 女人香蕉久久**毛片精品| 欧美一二区视频| 亚洲一区二区日本| 亚洲美女毛片| 亚洲欧洲美洲综合色网| 欧美 日韩 国产 一区|