實值型別語意
更新:2007 年 11 月
從 Managed Extensions for C++ 升級為 Visual C++ 2008 之後,實值型別 (Value Type) 語意已變更。
下列是在 Managed Extensions for C++ 規格中所使用的標準簡單實值型別:
__value struct V { int i; };
__gc struct R { V vr; };
在 Managed Extensions 中,一種實值型別可以擁有四種語法變數 (其中形式 2 和 3 的語意相同):
V v = { 0 }; // Form (1)
V *pv = 0; // Form (2) an implicit form of (3)
V __gc *pvgc = 0; // Form (3)
__box V* pvbx = 0; // Form (4) must be local
叫用繼承的虛擬方法
Form (1) 是標準值物件,而且已獲得適當的充分暸解,但是當某人嘗試叫用如 ToString() 之類之繼承的虛擬方法時例外。例如:
v.ToString(); // error!
如果要叫用此方法,由於並未在 V 中覆寫此方法,因此編譯器必須擁有相關基底類別之虛擬資料表的存取權。因為實值型別是缺少虛擬資料表之相關指標 (vptr) 的目前狀態中 (in-state) 儲存體,因此會要求 Box 處理 v。在 Managed Extensions 語言設計中,未支援隱含的 Boxing,但是必須由程式設計人員明確指定,如下所示
__box( v )->ToString(); // Managed Extensions: note the arrow
此設計背後的主要動機具備教學意義:程式設計人員必須能夠看見基礎機制,使其了解不在實值型別中提供執行個體的「代價」。如果 V 包含 ToString 的執行個體,就不一定需要 Boxing。
在新語法中,會移除明確 Boxing 物件的語彙複雜性,但不會移除 Boxing 本身的基礎代價:
v.ToString(); // new syntax
但是至於在 V 中未提供 ToString 方法的明確執行個體的代價,則是可能會誤導類別設計工具。慣用隱含 Boxing 的原因在於,通常類別設計工具只有一個,使用者卻有無限多個,如果沒有一個使用者可以自由修改 V,就可以消除可能很繁重的明確 Box。
判定是否在實值類別中提供 ToString 之覆寫執行個體的準則,是執行個體的使用頻率和位置。如果很少呼叫,當然在定義中就沒有什麼優勢。同樣地,如果在應用程式的非效能區域中呼叫,加入執行個體也不會適當地加入至應用程式的一般效能。此外,可以將追蹤處理代碼保持為 Boxed 值,則透過該處理代碼所做的呼叫就不需要 Boxing。
不再有實值類別預設建構函式
Managed Extensions 和新語法之間的另一項實值型別差異,則是後者已移除預設建構函式的支援。原因是在執行期間,有時 CLR 可以不需要叫用相關的預設建構函式,就直接建立實值型別的執行個體。也就是說,在 Managed Extensions 中,嘗試在實值型別中支援預設建構函式實際上無法獲得保證。如果缺乏保證,那麼徹底刪除支援似乎比在應用程式中不具決定性來好。
這並不像一開始看到的那麼糟。這是因為會自動去除實值型別的每個物件 (亦即每個型別都會初始化為其預設值)。因此,一定會定義本機執行個體的成員。就這點看來,無法定義一般預設建構函式真的一點都不算是損失,而且事實上,當由 CLR 執行時還更有效率。
問題是當 Managed Extensions 的使用者定義非一般預設建構函式時。這一點並未對應至新語法。建構函式中的程式碼將需要遷移至具名初始化方法,而此方法之後需要由使用者明確叫用。
新語法中的實值型別物件宣告則未變更。此項目的缺點是,實值型別基於下列理由並未符合原生型別包裝的要求:
在實值型別中沒有解構函式 (Destructor) 的支援。也就是說,無法自動化物件存留期結束時所觸發的一組動作。
原生類別只能當做指標內含於 Managed 型別中,此指標稍後會配置在原生堆積 (Heap) 上。
我們想要將小的原生類別包裝於實值型別而非參考型別中,以避免重複堆積配置:原生堆積是用來存放原生型別,而 CLR 堆積是用來存放 Managed 包裝函式。將原生類別包裝於實值型別中可以避免 Managed 堆積,但是無法自動化原生堆積記憶體的回收。參考型別是唯一可在其中包裝非一般原生類別的 Managed 型別。
內部指標
上述的 Form (2) 和 Form (3) 幾乎可以處理目前和以後的任何項目 (亦即任何 Managed 或原生項目)。例如,因此在 Managed Extensions 中允許下列所有項目:
__value struct V { int i; };
__gc struct R { V vr; };
V v = { 0 }; // Form (1)
V *pv = 0; // Form (2)
V __gc *pvgc = 0; // Form (3)
__box V* pvbx = 0; // Form (4)
R* r;
pv = &v; // address a value type on the stack
pv = __nogc new V; // address a value type on native heap
pv = pvgc; // we are not sure what this addresses
pv = pvbx; // address a boxed value type on managed heap
pv = &r->vr; // an interior pointer to value type within a
// reference type on the managed heap
所以,V* 可以處理下列範圍內的位址:在區域區塊 (因此可以依附)、在全域範圍、在原生堆積中 (例如,如果已刪除所處理的物件)、在 CLR 堆積中 (如果應該在記憶體回收期間遷移,因此應會追蹤),以及在 CLR 堆積的參考物件內部 (如果呼叫,也會透明地追蹤內部指標)。
在 Managed Extensions 中,無法區隔 V* 的原生部分,也就是說,將原生部分視為內含物,因而處理在 Managed 堆積上為物件或子物件定址的可能性。
在新語法中,實值型別指標分成兩種型別:限制在非 CLR 堆積位置的 V*,以及允許但是不需要 Managed 堆積中位址的內部指標 interior_ptr<V>。
// may not address within managed heap
V *pv = 0;
// may or may not address within managed heap
interior_ptr<V> pvgc = nullptr;
Managed Extensions 的 Form (2) 和 Form (3) 對應至 interior_ptr<V>。Form (4) 是追蹤控制代碼。它會處理已在 Managed 堆積中 Box 的整個物件。在新語法中會轉譯為 V^。
V^ pvbx = nullptr; // __box V* pvbx = 0;
Managed Extensions 中的下列宣告全部都會對應至新語法中的內部指標 (這些內部指標是 System 命名空間中的實值型別)。
Int32 *pi; // => interior_ptr<Int32> pi;
Boolean *pb; // => interior_ptr<Boolean> pb;
E *pe; // => interior_ptr<E> pe; // Enumeration
雖然內建型別的確是當做 System 命名空間中的型別別名,但卻未被視為 Managed 型別。因此下列對應會在 Managed Extensions 和新語法之間保持為真:
int * pi; // => int* pi;
int __gc * pi2; // => interior_ptr<int> pi2;
轉譯現有程式中的 V* 時,最穩健的策略就是永遠將它變成 interior_ptr<V>。這是在 Managed Extensions 中的處理方式。在新語法中,程式設計人員可以選擇指定 V* 而非內部指標,藉此將實值型別限制為 Unmanaged 堆積位址。如果轉譯程式時,可以為所有程式使用進行轉移終結,並且能夠確定沒有指派位址位於 Managed 堆積中,那麼保留為 V* 不會有問題。
Pin 指標
記憶體回收行程可以選擇將常駐於 CLR 堆積中的物件移動至堆積中的其他位置,而這通常是在壓縮階段進行。這項移動對於追蹤控制代碼、追蹤參考以及透明地更新這些實體的內部指標而言都不是問題。但是如果在執行階段環境以外,使用者已在 CLR 堆積上傳遞物件位址,這項移動就會造成問題。在這種情況下,短暫移動物件可能會引起執行階段錯誤。若要避免移動這類物件,必須針對物件外部使用的範圍,將物件區域性地 Pin 在位置上。
在 Managed Extensions 中,會使用 __pin 關鍵字限定指標宣告,藉以宣告「Pin 指標」(Pinning Pointer)。下列是從 Managed Extensions 規格稍做修改的範例:
__gc struct H { int j; };
int main()
{
H * h = new H;
int __pin * k = & h -> j;
// …
};
在新的語言設計中,是使用類似內部指標的語法宣告 Pin 指標。
ref struct H
{
public:
int j;
};
int main()
{
H^ h = gcnew H;
pin_ptr<int> k = &h->j;
// …
}
新語法中的 Pin 指標是特殊的內部指標。Pin 指標仍具有原本的條件約束。例如,不能當做參數使用,也不能當做方法的傳回型別,只能在區域物件 (Local Object) 上宣告。不過在新語法中還增加了一些其他條件約束。
Pin 指標的預設值是 nullptr,而不是 0。pin_ptr<> 不能被指派或是初始化為 0。需要將現有程式碼中的所有 0 的指派都變更為 nullptr。
Managed Extensions 中的 Pin 指標允許定址完整物件,如下列來自 Managed Extensions 規格的範例所示:
__gc class G {
public:
void incr(int* pi) { pi += 1; }
};
__gc struct H { int j; };
void f( G * g ) {
H __pin * pH = new H;
g->incr(& pH -> j);
};
在新語法中,不支援 Pin 由 new 運算式所傳回的完整物件。反而需要 Pin 內部成員的位址。例如,
ref class G {
public:
void incr(int* pi) { *pi += 1; }
};
ref struct H { int j; };
void f( G^ g ) {
H ^ph = gcnew H;
Console::WriteLine(ph->j);
pin_ptr<int> pj = &ph->j;
g->incr( pj );
Console::WriteLine(ph->j);
}