# Item 27: 將強制轉型減到最少
作者:Scott Meyers
譯者:fatalerror99 (iTePub's Nirvana)
發布:http://blog.csdn.net/fatalerror99/
C++ 的規則設計為保證不會發生類型錯誤。在理論上,如果你的程序想順利地通過編譯,你就不應該試圖對任何對象做任何不安全的或無意義的操作。這是一個非常有價值的保證,你不應該輕易地放棄它。
不幸的是,強制轉型破壞了類型系統。它會引起各種各樣的麻煩,其中一些容易被察覺,另一些則格外地微妙。如果你從 C,Java,或 C# 轉到 C++,請一定注意,因為強制轉型在那些語言中比在 C++ 中更有必要,危險也更少。但是 C++ 不是 C,也不是 Java,也不是 C#。在這一語言中,強制轉型是一個你必須全神貫注才可以靠近的特性。
我們就從回顧強制轉型的語法開始,因為對于同樣的強制轉型通常有三種不同的寫法。C 風格(C-style)強制轉型如下:
```
(T) expression // cast expression to be of type T
```
函數風格(Function-style)強制轉型使用這樣的語法:
```
T(expression) // cast expression to be of type T
```
這兩種形式之間沒有本質上的不同,它純粹就是一個把括號放在哪的問題。我把這兩種形式稱為舊風格(old-style)的強制轉型。
C++ 同時提供了四種新的強制轉型形式(通常稱為新風格的或 C++ 風格的強制轉型):
```
const_cast<T>(expression)
dynamic_cast<T>(expression)
reinterpret_cast<T>(expression)
static_cast<T>(expression)
```
每一種適用于特定的目的:
* const_cast 一般用于強制消除對象的常量性。它是唯一能做到這一點的 C++ 風格的強制轉型。
* dynamic_cast 主要用于執行“安全的向下轉型(safe downcasting)”,也就是說,要確定一個對象是否是一個繼承體系中的一個特定類型。它是唯一不能用舊風格語法執行的強制轉型。也是唯一可能有重大運行時代價的強制轉型。(過一會兒我再提供細節。)
* reinterpret_cast 是特意用于底層的強制轉型,導致實現依賴(implementation-dependent)(就是說,不可移植)的結果,例如,將一個指針轉型為一個整數。這樣的強制轉型在底層代碼以外應該極為罕見。在本書中我只用了一次,而且還僅僅是在討論你應該如何為裸內存(raw memory)寫一個調諧分配者(debugging allocator)的時候(參見 Item 50)。
* static_cast 可以被用于強制隱型轉換(例如,non-const 對象轉型為 const 對象(就像 Item 3 中的),int 轉型為 double,等等)。它還可以用于很多這樣的轉換的反向轉換(例如,void\* 指針轉型為有類型指針,基類指針轉型為派生類指針),但是它不能將一個 const 對象轉型為 non-const 對象。(只有 const_cast 能做到。)
舊風格的強制轉型依然合法,但是新的形式更可取。首先,在代碼中它們更容易識別(無論是人還是像 grep 這樣的工具都是如此),這樣就簡化了在代碼中尋找類型系統被破壞的地方的過程。第二,更精確地指定每一個強制轉型的目的,使得編譯器診斷使用錯誤成為可能。例如,如果你試圖使用一個 const_cast 以外的新風格強制轉型來消除常量性,你的代碼將無法編譯。
當我要調用一個 explicit 構造函數用來傳遞一個對象給一個函數的時候,大概就是我僅有的使用舊風格的強制轉換的時候。例如:
```
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
```
由于某種原因,有條不紊的對象創建感覺上不像一個強制轉型,所以在這個強制轉型中我多半會用函數風格的強制轉型代替 static_cast。反過來說,在你寫出那些導致 core dump 的代碼時,你通常都感覺你有合理的理由,所以你最好忽略你的感覺并始終都使用新風格的強制轉型。
很多程序員認為強制轉型除了告訴編譯器將一種類型看作另一種之外什么都沒做,但這是錯誤的。任何種類的類型轉換(無論是通過強制轉型的顯式的還是編譯器添加的隱式的)都會導致運行時的可執行代碼。例如,在這個代碼片斷中,
```
int x, y;
...
double d = static_cast<double>(x)/y; // divide x by y, but use
// floating point division
```
int x 到 double 的強制轉型理所當然要生成代碼,因為在大多數系統架構中,一個 int 的底層表示與 double 的不同。這可能還不怎么令人吃驚,但是下面這個例子可能會讓你稍微開一下眼:
```
class Base { ... };
class Derived: public Base { ... };
Derived d;
Base *pb = &d; // implicitly convert Derived* → Base*
```
這里我們只是創建了一個指向派生類對象的基類指針,但是有時候,這兩個指針的值并不相同。在當前情況下,會在運行時在 Derived\* 指針上應用一個偏移量以得到正確的 Base\* 指針值。
這后一個例子表明一個單一的對象(例如,一個類型為 Derived 的對象)可能會有不止一個地址(例如,它的被一個 Base\* 指針指向的地址和它的被一個 Derived\* 指針指向的地址)。這在 C 中就不會發生,也不會在 Java 中發生,也不會在 C# 中發生,它僅在 C++ 中發生。實際上,如果使用了多繼承,則一定會發生,但是在單繼承下也會發生。與其它事情合在一起,就意味著你應該總是避免對 C++ 如何擺放事物做出假設,你當然也不應該基于這樣的假設執行強制轉型。例如,將一個對象的地址強制轉型為 char\* 指針,然后對其使用指針運算,這幾乎總是會導致未定義行為。
但是請注意我說一個偏移量是“有時”被需要。對象擺放的方法和他們的地址的計算方法在不同的編譯器之間有所變化。這就意味著僅僅因為你的“我知道事物是如何擺放的”而使得強制轉型能工作在一個平臺上,并不意味著它們也能在其它平臺工作。這個世界被通過痛苦的道路學得這條經驗的可憐的程序員所充滿。
關于強制轉型的一件有趣的事是很容易寫出看起來對(在其它語言中也許是對的)實際上錯的東西。例如,許多應用框架(application framework)要求在派生類中實現虛成員函數時要首先調用它們的基類對應物。假設我們有一個 Window 基類和一個 SpecialWindow 派生類,它們都定義了虛函數 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
...
};
```
我突出了代碼中的強制轉型。(這是一個新風格的強制轉型,但是使用舊風格的強制轉型也于事無補。)正像你所期望的,代碼將 *this 強制轉型為一個 Window。因此調用 onResize 的結果就是調用 Window::onResize。你也許并不期待它沒有調用當前對象的那個函數!作為替代,強制轉型創建了一個 *this 的基類部分的新的,臨時的拷貝,然后調用這個拷貝的 onResize!上面的代碼沒有調用當前對象的 Window::onResize,然后再對這個對象執行 SpecialWindow 特有的動作——它在對當前對象執行 SpecialWindow 特有的動作之前,調用了當前對象的基類部分的一份拷貝的 Window::onResize。如果 Window::onResize 改變了當前對象(可能性并不小,因為 onResize 是一個 non-const 成員函數),當前對象并不會改變。作為替代,那個對象的一份拷貝被改變。如果 SpecialWindow::onResize 改變了當前對象,無論如何,當前對象將被改變,導致的境況是那些代碼使當前對象進入一種病態,沒有做基類的變更,卻做了派生類的變更。
解決方法就是消除強制轉型,用你真正想表達的來代替它。你不應該哄騙編譯器將 *this 當作一個基類對象來處理,你應該調用當前對象的 onResize 的基類版本。就是這樣:
```
class SpecialWindow: public Window {
public:
virtual void onResize() {
Window::onResize(); // call Window::onResize
... // on *this
}
...
};
```
這個例子也表明如果你發現自己要做強制轉型,這就是你可能做錯了某事的一個信號。在你想用 dynamic_cast 時尤其如此。
在探究 dynamic_cast 的設計意圖之前,值得留意的是很多 dynamic_cast 的實現都相當慢。例如,至少有一種通用的實現部分地基于對類名字進行字符串比較。如果你在一個位于四層深的單繼承體系中的對象上執行 dynamic_cast,在這樣一個實現下的每一個 dynamic_cast 都要付出相當于四次調用 strcmp 來比較類名字的成本。對于一個更深的或使用了多繼承的繼承體系,付出的代價會更加昂貴。一些實現用這種方法工作是有原因的(它們不得不這樣做以支持動態鏈接)。盡管如此,除了在普遍意義上警惕強制轉型外,在性能敏感的代碼中,你應該特別警惕 dynamic_casts。
對 dynamic_cast 的需要通常發生在這種情況下:你要在一個你確信為派生類的對象上執行派生類的操作,但是你只能通過一個基類的指針或引用來操控這個對象。有兩個一般的方法可以避免這個問題。
第一個,使用存儲著直接指向派生類對象的指針(通常是智能指針——參見 Item 13)的容器,從而消除通過基類接口操控這個對象的需要。例如,如果在我們的 Window/SpecialWindow 繼承體系中,只有 SpecialWindows 支持 blinking,對于這樣的做法:
```
class Window { ... };
class SpecialWindow: public Window {
public:
void blink();
...
};
typedef // see Item 13 for info
std::vector<std::tr1::shared_ptr<Window> > VPW; // on tr1::shared_ptr
VPW winPtrs;
...
for (VPW::iterator iter = winPtrs.begin(); // undesirable code:
iter != winPtrs.end(); // uses dynamic_cast
++iter) {
if (SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get()))
psw->blink();
}
```
設法用如下方法代替:
```
typedef std::vector<std::tr1::shared_ptr<SpecialWindow> > VPSW;
VPSW winPtrs;
...
for (VPSW::iterator iter = winPtrs.begin(); // better code: uses
iter != winPtrs.end(); // no dynamic_cast
++iter)
(*iter)->blink();
```
當然,這個方法不允許你在同一個容器中存儲所有可能的 Window 的派生類的指針。為了與不同的窗口類型一起工作,你可能需要多個類型安全(type-safe)的容器。
一個候選方法可以讓你通過一個基類的接口操控所有可能的 Window 派生類,就是在基類中提供一個讓你做你想做的事情的虛函數。例如,盡管只有 SpecialWindows 能 blink,在基類中聲明這個函數,并提供一個什么都不做的缺省實現或許是有意義的:
```
class Window {
public:
virtual void blink() {} // default impl is no-op;
... // see Item 34 for why
}; // a default impl may be
// a bad idea
class SpecialWindow: public Window {
public:
virtual void blink() { ... } // in this class, blink
... // does something
};
typedef std::vector<std::tr1::shared_ptr<Window> > VPW;
VPW winPtrs; // container holds
// (ptrs to) all possible
... // Window types
for (VPW::iterator iter = winPtrs.begin();
iter != winPtrs.end();
++iter) // note lack of
(*iter)->blink(); // dynamic_cast
```
無論哪種方法——使用類型安全的容器或在繼承體系中上移虛函數——都不是到處適用的,但在很多情況下,它們提供了 dynamic_casting 之外另一個可行的候選方法。當它們可用時,你應該加以利用。
你應該絕對避免的一件東西就是包含了極聯 dynamic_casts 的設計,也就是說,看起來類似這樣的任何東西:
```
class Window { ... };
... // derived classes are defined here
typedef std::vector<std::tr1::shared_ptr<Window> > VPW;
VPW winPtrs;
...
for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter)
{
if (SpecialWindow1 *psw1 =
dynamic_cast<SpecialWindow1*>(iter->get())) { ... }
else if (SpecialWindow2 *psw2 =
dynamic_cast<SpecialWindow2*>(iter->get())) { ... }
else if (SpecialWindow3 *psw3 =
dynamic_cast<SpecialWindow3*>(iter->get())) { ... }
...
}
```
這樣的 C++ 會生成的代碼又大又慢,而且很脆弱,因為每次 Window 類繼承體系發生變化,所有這樣的代碼都要必須被檢查,以確認是否需要更新。(例如,如果增加了一個新的派生類,在上面的極聯中或許就需要加入一個新的條件分支。)看起來類似這樣的代碼應該總是用基于虛函數的調用的某種東西來替換。
好的 C++ 極少使用強制轉型,但在通常情況下完全去除也不實際。例如,第 118 頁從 int 到 double 的強制轉型,就是對強制轉型的合理運用,雖然它并不是絕對必要。(那些代碼應該被重寫,聲明一個新的類型為 double 的變量,并用 x 的值進行初始化。)就像大多數可疑的結構成分,強制轉型應該被盡可能地隔離,典型情況是隱藏在函數內部,用函數的接口保護調用者遠離內部的污穢的工作。
Things to Remember
* 避免強制轉型的隨時應用,特別是在性能敏感的代碼中應用 dynamic_casts,如果一個設計需要強制轉型,設法開發一個沒有強制轉型的侯選方案。
* 如果必須要強制轉型,設法將它隱藏在一個函數中。客戶可以用調用那個函數來代替在他們自己的代碼中加入強制轉型。
* 盡量用 C++ 風格的強制轉型替換舊風格的強制轉型。它們更容易被注意到,而且他們做的事情也更加明確。
- Preface(前言)
- Introduction(導言)
- Terminology(術語)
- Item 1: 將 C++ 視為 federation of languages(語言聯合體)
- Item 2: 用 consts, enums 和 inlines 取代 #defines
- Item 3: 只要可能就用 const
- Item 4: 確保 objects(對象)在使用前被初始化
- Item 5: 了解 C++ 為你偷偷地加上和調用了什么函數
- Item 6: 如果你不想使用 compiler-generated functions(編譯器生成函數),就明確拒絕
- Item 7: 在 polymorphic base classes(多態基類)中將 destructors(析構函數)聲明為 virtual(虛擬)
- Item 8: 防止因為 exceptions(異常)而離開 destructors(析構函數)
- Item 9: 絕不要在 construction(構造)或 destruction(析構)期間調用 virtual functions(虛擬函數)
- Item 10: 讓 assignment operators(賦值運算符)返回一個 reference to *this(引向 *this 的引用)
- Item 11: 在 operator= 中處理 assignment to self(自賦值)
- Item 12: 拷貝一個對象的所有組成部分
- Item 13: 使用對象管理資源
- Item 14: 謹慎考慮資源管理類的拷貝行為
- Item 15: 在資源管理類中準備訪問裸資源(raw resources)
- Item 16: 使用相同形式的 new 和 delete
- Item 17: 在一個獨立的語句中將 new 出來的對象存入智能指針
- Item 18: 使接口易于正確使用,而難以錯誤使用
- Item 19: 視類設計為類型設計
- Item 20: 用 pass-by-reference-to-const(傳引用給 const)取代 pass-by-value(傳值)
- Item 21: 當你必須返回一個對象時不要試圖返回一個引用
- Item 22: 將數據成員聲明為 private
- Item 23: 用非成員非友元函數取代成員函數
- Item 24: 當類型轉換應該用于所有參數時,聲明為非成員函數
- Item 25: 考慮支持不拋異常的 swap
- Item 26: 只要有可能就推遲變量定義
- Item 27: 將強制轉型減到最少
- Item 28: 避免返回對象內部構件的“句柄”
- Item 29: 爭取異常安全(exception-safe)的代碼
- Item 30: 理解 inline 化的介入和排除
- Item 31: 最小化文件之間的編譯依賴
- Item 32: 確保 public inheritance 模擬 "is-a"
- Item 33: 避免覆蓋(hiding)“通過繼承得到的名字”
- Item 34: 區分 inheritance of interface(接口繼承)和 inheritance of implementation(實現繼承)
- Item 35: 考慮可選的 virtual functions(虛擬函數)的替代方法
- Item 36: 絕不要重定義一個 inherited non-virtual function(通過繼承得到的非虛擬函數)
- Item 37: 絕不要重定義一個函數的 inherited default parameter value(通過繼承得到的缺省參數值)
- Item 38: 通過 composition(復合)模擬 "has-a"(有一個)或 "is-implemented-in-terms-of"(是根據……實現的)
- Item 39: 謹慎使用 private inheritance(私有繼承)
- Item 40: 謹慎使用 multiple inheritance(多繼承)
- Item 41: 理解 implicit interfaces(隱式接口)和 compile-time polymorphism(編譯期多態)
- Item 42: 理解 typename 的兩個含義
- Item 43: 了解如何訪問 templatized base classes(模板化基類)中的名字
- Item 44: 從 templates(模板)中分離出 parameter-independent(參數無關)的代碼
- Item 45: 用 member function templates(成員函數模板) 接受 "all compatible types"(“所有兼容類型”)
- Item 46: 需要 type conversions(類型轉換)時在 templates(模板)內定義 non-member functions(非成員函數)
- Item 47: 為類型信息使用 traits classes(特征類)
- Item 48: 感受 template metaprogramming(模板元編程)
- Item 49: 了解 new-handler 的行為
- Item 50: 領會何時替換 new 和 delete 才有意義
- Item 51: 編寫 new 和 delete 時要遵守慣例
- Item 52: 如果編寫了 placement new,就要編寫 placement delete
- 附錄 A. 超越 Effective C++
- 附錄 B. 第二和第三版之間的 Item 映射