Item 27: 最少化 casting(強制轉型)
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
C++ 的規則被設計為保證不會發生類型錯誤。在理論上,如果你的程式想順利地通過編譯,它就不應該試圖針對任何 objects 做任何不安全的或無意義的操作。這是一個非常有價值的保證,你不應該輕易地放棄它。
不幸的是,casts(強制轉型)攪亂了 type system(類型系統)。它會導致各種各樣的麻煩,其中一些容易被察覺,另一些則格外地微妙。如果你從 C,Java,或 C# 轉到 C++,請一定注意,因為 casting(強制轉型)在那些語言中比在 C++ 中更有必要,危險也更少。但是 C++ 不是 C,也不是 Java,也不是 C#。在這一語言中,casting(強制轉型)是一個需要你帶有極大的敬畏之心才可以靠近的特性。
我們就從回顧 casting(強制轉型)文法開始,因為對於同樣的 cast(強制轉型)通常有三種不同的寫法。C-style casts(C 風格強制轉型)如下:
(T) expression // cast expression to be of type T
Function-style casts(函數風格強制轉型)使用這樣的文法:
T(expression) // cast expression to be of type T
這兩種形式之間沒有含義上的不同,它純粹就是一個把括弧放在哪的問題。我把這兩種形式稱為 old-style casts(舊風格強制轉型)。
C++ 同時提供了四種新的 cast(強制轉型)形式(通常稱為 new-style(新風格)或 C++-style casts(C++ 風格強制轉型)):
const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
每一種適用於特定的目的:
- const_cast 一般用於強制消除 objects 的 constness(常量性)。它是唯一能做到這一點的 C++-style cast(C++ 風格強制轉型)。
- dynamic_cast 主要用於執行 “safe downcasting”(“安全的向下轉型”),也就是說,用於確定一個 object 是否是一個 inheritance hierarchy(繼承體系)中的一個特定類型。它是唯一不能用 old-style(舊風格)文法執行的 cast(強制轉型)。也是唯一可能有很大運行時成本的 cast(強制轉型)。(過一會兒我會提供與此相關的細節。)
- reinterpret_cast 是特意用於底層的導致 implementation-dependent(實現依賴)(也就是說,不可移植)的結果的 casts(強制轉型),例如,將一個 pointer 轉型為一個 int。這樣的 casts(強制轉型)在底層代碼以外應該極為罕見。在本書中我只用了一次,而且還僅僅是在討論你應該如何為 raw memory(裸記憶體)寫一個 debugging allocator(可調試分配器)的時候(參見 Item 50)。
- static_cast 可以被用於 force implicit conversions(強制隱式轉換)(例如,non-const object 到 const object(就像 Item 3 中的),int 到 double,等等)。它還可以用於執行多數這樣的轉換的反向轉換(例如,void* 指標到有類型指標,pointer-to-base(基類指標)到 pointer-to-derived(衍生類別指標)),雖然它不能將 const objects 轉型為 non-const objects。(只有 const_cast 能做到這一點。)
old-style casts(舊風格強制轉型)依然是合法的,但是新的形式更可取。首先,在代碼中它們更容易識別(對人和對像 grep 這樣的工具都是如此),這樣就簡化了在代碼中尋找 type system(類型系統)被攪亂的地方的過程。第二,更精確地指定每一個 cast(強制轉型)的目的,使得編譯器診斷使用錯誤成為可能。例如,如果你試圖使用一個 const_cast 以外的 new-style cast(新風格強制轉型)來消除 constness(常量性),你的代碼將無法編譯。
當我想要調用一個 explicit constructor(顯式建構函式)用來傳遞一個 object 給一個函數的時候,大概就是我僅有的使用 old-style cast(舊風格強制轉型)的時候。例如:
class Widget {
public:
explicit Widget(int size);
...
};
void doSomeWork(const Widget& w);
doSomeWork(Widget(15)); // create Widget from int
// with function-style cast
doSomeWork(static_cast<Widget>(15)); // create Widget from int
// with C++-style cast
由於某種原因,有條不紊的 object creation(對象建立)感覺上不像一個 cast(強制轉型),所以在此情況下我或許會用 function-style cast(函數風格強制轉型)取代 static_cast。還有,在你寫出那些導致 core dump 的代碼時,你通常都感覺你有很完美的理由,所以你最好忽略你的感覺並始終都使用 new-style casts(新風格強制轉型)。
很多程式員認為 casts(強制轉型)除了告訴編譯器將一種類型看作另一種之外什麼都沒做,但這是錯誤的。任何種類的類型轉換(無論是通過 casts(強制轉型)的 explicit(顯式)的還是編譯器添加的 implicit(隱式)的)常常導致運行時的可執行代碼。例如,在這個代碼片斷中,
int x, y;
...
double d = static_cast<double>(x)/y; // divide x by y, but use
// floating point division
int x 到一個 double 的 cast(強制轉型)理所當然要產生代碼,因為在大多數系統架構中,一個 int 的底層表示與一個 double 的不同。這或許還不怎麼令人驚訝,但是下面這個例子可能會讓你稍微開一下眼:
class Base { ... };
class Derived: public Base { ... };
Derived d;
Base *pb = &d; // implicitly convert Derived* → Base*
這裡我們只是建立了一個指向一個 derived class object(衍生類別對象)的 base class pointer(基類指標),但是有時候,這兩個指標的值並不相同。在這種情況下,會在運行時在 Derived* 指標上應用一個位移量以得到正確的 Base* 指標值。
這後一個例子表明一個單一的 object(例如,一個 Derived 類型的 object)可能會有不止一個地址(例如,它的被一個 Base* 指標指向的地址和它的被一個 Derived* 指標指向的地址)。這在 C 中就不會發生,也不會在 Java 中發生,也不會在 C# 中發生,它僅在 C++ 中發生。實際上,如果使用了 multiple inheritance(多繼承),則一定會發生,但是在 single inheritance(單繼承)下也會發生。與其它事情合在一起,就意味著你通常應該避免對 C++ 如何擺放東西做出假設,你當然也不應該基於這樣的假設執行 casts(強制轉型)。例如,將一個 object 的地址強制轉型為 char* 指標,然後對其使用指標運算,這幾乎總是會導致 undefined behavior(未定義行為)。
但是請注意我說一個位移量是“有時”被需要。objects 被擺放的方法和他們的地址的被計算的方法在不同的編譯器之間有所變化。這就意味著僅僅因為你的“我知道東西是如何擺放”的 casts(強制轉型)能工作在一個平台上,並不意味著它們也能在其它平台工作。這個世界被通過痛苦的道路學得這條經驗的可憐的程式員所充滿。
關於 casts(強制轉型)的一件有趣的事是很容易寫出看起來對(在其它語言中也許是對的)實際上錯的東西。例如,許多 application framework(應用程式架構)要求 derived classes(衍生類別)中 virtual member function(虛擬成員函數)的實現要首先調用它們的 base class(基類)的對應物。假設我們有一個 Window base class(基類)和一個 SpecialWindow derived class(衍生類別),它們都定義了 virtual function(虛擬函數)onResize。進一步假設 SpecialWindow 的 onResize 被期望首先調用 Window 的 onResize。這就是實現這個的一種方法,它看起來正確實際並不正確:
class Window { // base class
public:
virtual void onResize() { ... } // base onResize impl
...
};
class SpecialWindow: public Window { // derived class
public:
virtual void onResize() { // derived onResize impl;
static_cast<Window>(*this).onResize(); // cast *this to Window,
// then call its onResize;
// this doesn't work!
... // do SpecialWindow-
} // specific stuff
...
};
我突出了代碼中的 cast(強制轉型)。(這是一個 new-style cast(新風格強制轉型),但是使用一個 old-style cast(舊風格強制轉型)也於事無補。)正像你所期望的,代碼將 *this 強制轉型為一個 Window。因此調用 onResize 的結果就是調用 Window::onResize。你可能並不期待它沒有在 current object(當前對象)上調用那個函數!作為替代,cast(強制轉型)建立了一個新的,臨時的 *this 的 base class part(基類部分)的 copy(拷貝),然後在這個拷貝上調用 onResize!上面的代碼沒有在 current object(當前對象)上調用 Window::onResize,然後再在這個 object 上執行 SpecialWindow 特有的動作——它在在 current object(當前對象)上執行 SpecialWindow 特有的動作之前,在一份 current object(當前對象)的 copy of the base class part(基類部分的拷貝)上調用了 Window::onResize。如果 Window::onResize 改變了 current object(當前對象)(可能性並不小,因為 onResize 是一個 non-const member function(成員函數)),current object(當前對象)並不會改變。作為替代,那個 object 的一份 copy(拷貝)被改變。然而,如果 SpecialWindow::onResize 改變了 current object(當前對象),current object(當前對象)將被改變,導致的境況是那些代碼使 current object(當前對象)進入一種病態,沒有做 base class(基類)的變更,卻做了 derived class(衍生類別)的變更。
解決方案就是消除 cast(強制轉型),用你真正想表達的來代替它。你不需要哄騙編譯器將 *this 當作一個 base class object(基類對象),你需要在 current object(當前對象)上調用 onResize 的 base class version(基類版本)。就是這樣:
class SpecialWindow: public Window {
public:
virtual void onResize() {
Window::onResize(); // call Window::onResize
... // on *this
}
...
};
這個例子也表明如果你發現自己要做 cast(強制轉型),這就是你可能用錯誤的方法處理某事的一個訊號。在你想用 dynamic_cast 時尤其如此。
(本片未完,點擊此處,接下篇)