Item 27: Minimize casting.
C++的類型檢查只在編譯時執行,運行時沒有類型錯誤的概念。 理論上講只要你的代碼可以編譯那麼就運行時就不會有不安全的操作發生。 但C++允許類型轉換,也正是類型轉換破壞了理論上的類型系統。
在C#,Java等語言中類型轉換會更加必要和頻繁,但它們總是安全的。C++則不然, 這要求我們在類型轉換時格外小心。C++中的類型轉換有三種方式:
C風格的類型轉換:
(T) expression
函數風格的類型轉換:
T(expression)
C++風格的類型轉換。包括const_cast
, dynamic_cast
, reinterpret_cast
, static_cast
。
const_cast
用於去除常量性質。dynamic_cast
用於安全向下轉型,有運行時代價。reinterpret_cast
低級類型轉換,它實現相關的因而不可移植。static_cast
強制進行隱式類型轉換,例如int
到double
,非常量到常量(反過來不可!只有const_cast
可以做這個)等。
C風格轉型和函數風格轉型沒有區別,只是括號的位置不一樣。C++風格的類型轉換語義更加明確(編譯器會做更詳細的檢查)不容易誤用, 另外也更容易在代碼中找到那些破壞了類型系統的地方。所以盡量用C++風格的轉型,比如下面代碼中,後者是更好的習慣:
func(Widget(15));
func(static_cast(15));
很多人認為類型轉換只是告訴編譯器把它當做某種類型。事實上並非如此,比如最常見的數字類型轉換:
int x,y;
double d = static_cast(x)/y;
上述的類型轉換一定是產生了更多的二進制代碼,因為多數平台中int
和double
的底層表示並不一樣。再來一個例子:
Derived d; // 子類對象
Base *pb = &d; // 父類指針
同一對象的子類指針和父類指針有時並不是同一地址(取決於編譯器和平台),而運行時代碼需要計算這一偏移量。 一個對象會有不同的地址是C++中獨有的現象,所以不要對C++對象的內存分布做任何假設,更不要基於該假設做類型轉換。 這樣可以避免一些未定義行為。Scott Meyers如是說:
The world is filled with woeful programmers who've learned this lesson the hard way.
C++類型轉換有趣的一點在於,很多時候看起來正確事實上卻是錯誤的。比如SpecialWindow
繼承自Window
, 它的onResize
需要調用父類的onResize
。一個實現方式是這樣的:
class SpecialWindow: public Window{
public:
virtual void onResize(){
// Window onResize ...
static_cast(*this).onResize();
// SpecialWindow onResize ...
}
};
這樣寫的結果是當前對象父類部分被拷貝(調用了Window
的拷貝構造函數),並在這個副本上調用onResize
。 當前對象的Window::onResize
並未被調用而SpetialWindow::onResize
的後續代碼被執行了, 如果後續代碼修改了屬性值,那麼當前對象將處於無效的狀態。正確的方法也很顯然:
class SpecialWindow: public Window{
public:
virtual void onResize(){
// Window onResize ...
Window::onResize();
// SpecialWindow onResize ...
}
};
這個例子說明,當你想要轉型時可能已經誤入歧途了。此時需要仔細考慮一下是否真的需要轉型?
在一般的實現中dynamic_cast
會逐級地比較類名。比如4級的繼承結構,dynamic_cast
strcmp
才能確定最終的那個子類型。 所以在性能關鍵的部分盡量避免dynamic_cast
。通常有兩種途徑:
使用子類的容器,而不是父類容器。比如
vector v;
dynamic_cast(v[0]).blink();
換成子類容器就好了嘛;
vector v;
v[0].blink();
但這樣你就不能在容器裡放其他子類的對象了,你可以定義多個類型安全的容器來分別存放這些子類的對象。
通過虛函數提供統一的父類接口。比如:
class Window{
public:
virtual void blink();
...
};
class SpecialWindow: public Window{
public:
virtual void blink();
...
};
vector v;
v[0].blink();
這兩個方法並不能解決所有問題,但如果可以解決你的問題,你就應該采用它們來避免類型轉換。 這取決於你的使用場景,但可以確定的是,連續的dynamic_cast
一定要避免,比如這樣:
if(SpecialWindow1 *p = dynamic_cast(it->get()){...}
else if(SpecialWindow2 *p = dynamic_cast(it->get()){...}
else if(SpecialWindow3 *p = dynamic_cast(it->get()){...}
...
這樣的代碼性能極差,而且又代碼維護問題:當你又來一個SpecialWindow4
的時候你需要再次找到這段代碼來進行擴展。 使用虛函數完全可以替代上述的實現。
好的C++很少使用轉型。但轉型是不可避免的,比如int
到double
的轉換就很好用,省的定義一個新的double
並用`int來初始化它。 像其他的可讀性差的代碼一樣,你應該把轉型封裝到函數裡面去。總之:
dynamic_cast
。可以通過更好的設計來避免轉型。