safe_cast<> 的轉換通知和簡介
從 Managed Extensions for C++ 升級為 Visual C++ 2010 之後,轉型標記法已經變更。
相較於設計初始結構,修改現有結構是大不相同而且更為困難的工作, 不僅自由度比較低,而且解決方案往往是理想的重整與現有結構相依性條件下可行的重整,兩者之間的折衷。
語言擴充功能是另一個例子。 在 1990 年代初期,當物件導向程式設計變成重要的開發架構時,C++ 中對於型別安全向下轉換功能的需求就變得很急迫。 向下轉換是基底類別指標 (或是指標參考或衍生類別參考) 的使用者明確轉換, 向下轉型需要明確轉型。 原因是基底類別指標的實際型別是執行階段的一個面向,因此編譯器無法進行檢查; 或者,換個方式來講,向下轉換功能就好像是虛擬函式呼叫,它需要某種形式的動態解決方式。 這會引發兩個問題:
為什麼在物件導向開發架構中必須使用向下轉換? 難道虛擬函式機制還不夠嗎? 也就是說,為什麼不能指稱若需要使用向下轉型 (或任何形式的轉型) 就是設計缺失呢?
為什麼在 C++ 中支援向下轉換會是一個問題? 畢竟,在像 Smalltalk (或者是後續的 Java 和 C#) 這種物件導向的程式語言中,它並不會造成問題。 為什麼在 C++ 中支援向下轉換功能會這麼困難呢?
虛擬函式代表一個型別系列共同的型別相依演算法 (我們不考慮介面,ISO-C++ 不支援介面,但在 CLR 程式設計中可以使用介面,而且它是相當有趣的設計替代方式)。 該系列的設計通常是由類別階層來表示,在類別階層中,有一個宣告通用介面 (虛擬函式) 的抽象基底類別,以及一組代表應用程式定義域中實際系列型別的具象衍生類別。
舉例來說,CGI (Computer Generated Imagery) 應用程式定義域中的 Light 階層會具有通用屬性,例如 color、intensity、position、on、off 等等。 您可以經由通用介面來控制多盞燈光,而不須擔心某特定一盞燈是聚光燈、方向燈、輻射燈 (例如太陽) 或者是穀倉門的燈。 在這種情況下,並不必為了執行其虛擬介面,而向下轉型至特定燈光類型。 但是在實際環境中,速度重於一切。 如果向下轉型並明確叫用每一個方法,就可以執行呼叫的內嵌作業而不必使用虛擬機制,那麼您可能就會這麼做。
因此,在 C++ 中使用向下轉換的其中一個原因,就是要藉著抑制虛擬機制以求得執行階段效能的顯著提升 (請注意,這種手動最佳化的 Automation 是目前正在研究的區域, 不過,若和取代 register 或 inline 關鍵字的明確使用相比,要解決其使用問題會比較困難)。
向下轉換的第二個原因在於多型 (Polymorphism) 的雙重性質。 您可以將多型想成是分割為被動與動態的一對形式。
虛擬引動過程 (與向下轉換功能) 代表動態使用多型:在程式執行中的該特定執行個體依據基底類別指標的實際型別來執行動作。
不過,將衍生類別物件指派給它的基底類別指標是多型的被動形式,它是以傳輸機制的方式來使用多型。 這是 Object 的主要用法,例如,在前置泛型 CLR 程式設計中。 以被動方式使用時,選來用於傳輸與儲存的基底類別指標通常會提供非常抽象的介面, 例如,Object 大致上會經由它的介面提供五個方法;任何其他特定行為都需要明確的向下轉換, 例如,如果想要調整聚光燈的角度或是減弱的速率,就必須明確地向下轉型。 一系列子型別內的虛擬介面實務上無法成為其許多子項的所有可能方法的超集,因此物件導向的程式語言中一律需要向下轉換功能。
如果物件導向的程式語言中需要安全的向下轉換功能,那為什麼 C++ 要花那麼久的時間才加入此功能? 問題是在於如何讓關於指標之執行階段型別的資訊可供使用。 就虛擬函式而言,執行階段資訊由編譯器分兩部分設定:
類別物件包含額外的虛擬資料表指標成員 (在類別物件開頭或結尾,這本身有個有趣的故事) 來為適當的虛擬資料表定址。 例如,聚光燈物件為聚光燈虛擬資料表定址,方向燈物件為方向燈虛擬資料表定址,依此類推。
每個虛擬函式在資料表中都有相關聯的固定位置,而要叫用的實際執行個體則由資料表中儲存的位址代表。 例如,虛擬 Light 解構函式可能和位置 0 相關聯,而 Color 則和位置 1 相關聯,依此類推。 這是一個有效但可能沒有彈性的策略,因為它是設定於編譯階段,並且產生最少的額外負荷。
那麼,問題就在於不論是藉由加入第二個位址,或是直接加入某種型別編碼方式,要如何不變更 C++ 指標的大小,就可以將型別資訊供指標使用。 對於那些決定不使用物件導向開發架構的程式設計人員 (仍然是主流的使用者社群) 和程式而言,這可能令人無法接受。 另一個可能性是為多型類別型別引入特殊的指標,但這可能會讓人感到混淆,而且會難以混合兩者,尤其是指標算術問題。 同樣令人無法接受的是要維護執行階段資料表,來建立每一個指標與其目前的關聯型別之間的關聯,並以動態方式進行更新。
接下來的問題是,兩個使用者社群雙方的程式設計目標雖然不同,卻一樣具有正當性, 解決方案必須在這兩個社群之間取得折衷,讓每一個社群不只能夠施展抱負,還能夠互通。 這表示各方所提出的解決方案很可能無法實行,而最終實作的方案就無法臻至完美。 實際的解決方案是以多型類別的定義為中心:多型類別是指包含虛擬函式的類別。 多型類別支援動態型別安全向下轉換, 這解決了「以位址方式維護指標」(maintain-the-pointer-as-address) 的問題,因為所有多型類別都會在它們相關聯的虛擬資料表中包含該額外的指標成員, 因此,相關聯的型別資訊可以儲存在擴充的虛擬資料表結構中。 型別安全向下轉換的成本 (幾乎) 已局部化至此功能的使用者。
型別安全向下轉型的下一個問題是它的語法, 因為它是轉型 (Cast),所以向 ISO-C++ 委員會提出的原始提案是使用未經裝飾的轉型語法,如這個範例所示:
spot = ( SpotLight* ) plight;
但委員會駁回這項提案,因為它無法讓使用者控制轉換的成本。 如果動態型別安全向下轉型和先前不安全但靜態的轉型標記法的語法相同,那麼它會變成替代品,而且當它不必要而且可能代價太高時,使用者將無法抑制執行階段的額外負荷。
一般而言,在 C++ 中,一定有一個機制可以用來抑制編譯器支援的功能, 例如,我們可以使用類別範圍運算子 (Box::rotate(angle)) 或透過類別物件 (而不是該類別的指標或參考) 叫用虛擬方法,來關閉虛擬機制。 此語言並不需要後一種抑制方式,它只是實作品質的問題,類似在下列宣告形式中抑制建構暫存區:
// compilers are free to optimize away the temporary
X x = X::X( 10 );
因此,原先的提案只好收回做進一步的考量,在考慮過幾個替代的標記法後,最後重新呈現給委員會的是 (?type) 這個形式,來表示它的不確定,也就是,動態的本質。 這讓使用者能夠在兩個表單之間切換 (靜態或動態),但沒有人覺得這很值得高興, 所以,它又被帶回來繼續規劃。 第三次並且是成功的一次,就是現在的標準 dynamic_cast<type> 標記法,它被改成具有廣義性質的一組四個新樣式的轉換標記法。
在 ISO-C++ 中,當 dynamic_cast 套用至不適當的指標型別時,會傳回 0,而套用至參考型別時,會擲回 std::bad_cast 例外狀況。 在 Managed Extensions for C++ 中,若將 dynamic_cast 套用至 Managed 參考型別 (因為它的指標表示) 一律會傳回 0。 __try_cast<type> 的引用方式類似於 dynamic_cast 的例外狀況擲回變數,只不過當轉換失敗時,它擲回的是 System::InvalidCastException。
public __gc class ItemVerb;
public __gc class ItemVerbCollection {
public:
ItemVerb *EnsureVerbArray() [] {
return __try_cast<ItemVerb *[]>
(verbList->ToArray(__typeof(ItemVerb *)));
}
};
在新的語法中,__try_cast 已經重新轉換為 safe_cast。 下面是使用新語法的相同程式碼片段:
public ref class ItemVerb;
public ref class ItemVerbCollection {
public:
array<ItemVerb^>^ EnsureVerbArray() {
return safe_cast<array<ItemVerb^>^>
( verbList->ToArray( ItemVerb::typeid ));
}
};
在 Managed 世界中,限制程式設計人員不能以未驗證程式碼的方式在型別之間進行轉型,以促成可驗證的程式碼,這點很重要。 這是由新語法所代表的動態程式設計開發架構的一個重要面向。 因此,舊樣式轉換的執行個體會在內部重新轉換為執行階段轉換,如下所示:
// internally recast into the
// equivalent safe_cast expression above
( array<ItemVerb^>^ ) verbList->ToArray( ItemVerb::typeid );
另一方面,因為多型提供了主動與被動兩種模式,所以有時候必須執行向下轉型,才能取得子型別的非虛擬 API 的存取權。 例如,若某類別的成員想要定址階層內的任何型別 (以被動多型做為傳輸機制),但該成員在特定程式內容中的實際執行個體是已知的,則該成員就可能發生這種情形。 在這種情況下,於執行階段檢查轉型可能是令人無法接受的額外負荷。 如果新的語法是用來做為 Managed 系統程式設計語言,它必須提供某種可允許編譯階段 (亦即,靜態) 向下轉換的方法, 因此,static_cast 標記法的應用仍然可以是編譯階段向下轉型:
// ok: cast performed at compile-time.
// No run-time check for type correctness
static_cast< array<ItemVerb^>^>(verbList->ToArray(ItemVerb::typeid));
問題就在於無法保證執行 static_cast 的程式設計人員是對的而且意圖良好;也就是說,無法迫使 Managed 程式碼成為可驗證的程式碼。 比起在原生架構下,這在動態程式設計開發架構下是比較急迫的考慮事項,但是在系統程式設計語言中這並不足夠讓使用者無法在靜態與執行階段轉換之間進行切換。
然而,新語法中有一個效能陷阱和圈套。 在原生程式設計中,舊樣式轉換標記法和新樣式 static_cast 標記法之間並沒有效能上的差異, 但是在新語法中,使用舊樣式轉型標記法所耗費的資源遠比使用新樣式 static_cast 標記法為多。 原因在於,編譯器內部會將舊樣式標記法的用法轉換成會擲回例外狀況的執行階段檢查。 此外,它還會變更程式碼的執行路線,因為它會導致無法攔截的例外狀況,造成應用程式當掉,這或許是明智的做法,但如果使用 static_cast 標記法,相同的錯誤並不會造成該例外狀況。 有人或許會認為,這有助於敦促使用者使用新樣式標記法。 但只有在失敗時才有用,否則它只會造成使用舊樣式標記法的程式顯著降低執行速度,而沒有任何讓人明瞭的原因,類似 C 程式設計人員易犯的下列錯誤:
// pitfall # 1:
// initialization can remove a temporary class object,
// assignment cannot
Matrix m;
m = another_matrix;
// pitfall # 2: declaration of class objects far from their use
Matrix m( 2000, 2000 ), n( 2000, 2000 );
if ( ! mumble ) return;