讀書筆記 effective c++ Item 27 盡量少使用轉型(casting)。本站提示廣大學習愛好者:(讀書筆記 effective c++ Item 27 盡量少使用轉型(casting))文章只能為提供參考,不一定能成為您想要的結果。以下是讀書筆記 effective c++ Item 27 盡量少使用轉型(casting)正文
C++設計的規則是用來保證使類型相關的錯誤不再可能出現。理論上來說,如果你的程序能夠很干淨的通過編譯,它就不會嘗試在任何對象上執行任何不安全或無意義的操作。這個保證很有價值,不要輕易放棄它。
不幸的是,casts顛覆了類型系統。它導致了各種麻煩的出現,一些很容易識別,一些卻很狡猾(不容易被識別)。如果你以前使用過C,java或者C#,你就需要注意了,因為在這些語言中casting是更加必不可少的,但卻比C++更安全。C++不是C,不是java也不是C#,在C++中,你需要懷著極大的敬意來使用casting。
1. 新舊風格cast回顧 1.1 舊風格cast先讓我們回顧一下casting的語法,通常有三種不同的方法來實現同一個cast。C風格的casts如下:
1 (T) expression // cast expression to be of type T
函數風格的casts使用下面的語法:
1 T(expression) // cast expression to be of type T
上面的兩種形式在意義上沒有區別,只是放括號的地方不一樣。我們將這兩種形式的casts叫做舊式風格的casts。
1.2 C++風格的castC++同樣提供四種新的casts形式(C++風格的casts):
1 const_cast<T>(expression) 2 dynamic_cast<T>(expression) 3 reinterpret_cast<T>(expression) 4 static_cast<T>(expression)
每種方法都有獨特的用途:
舊式風格的casts仍然合法,但是新風格的更好。第一,在代碼中它們更加容易被辨別(對於人或者工具來說),因此簡化了在代碼中尋找轉型動作的過程。第二,每個cast更加特別的使用用途使得編譯器能夠診斷出使用錯誤成為可能。譬如,如果你使用其它3個cast而不是const_cast來去除常量的常量性,你的代碼無法通過編譯。
我使用舊式風格cast的唯一地方是當我想通過調用一個explici構造函數來為一個函數傳遞一個對象的時候。比如:
1 class Widget { 2 public: 3 explicit Widget(int size); 4 ... 5 }; 6 void doSomeWork(const Widget& w); 7 doSomeWork(Widget(15)); // create Widget from int 8 // with function-style cast 9 10 doSomeWork(static_cast<Widget>(15)); // create Widget from int 11 // with C++-style cast
從某種意義上來說,這種對象的創建不像是一個cast,所以使用了函數風格的cast而不是static_cast。(這兩種方法做了相同的事情:創建一個臨時Widget對象然後傳遞給doSomeWork。)需要再說一遍,使用舊式轉型實現的代碼往往當時感覺很合理,但日後可能出現core dump,所以最好忽略這種感覺,總是使用新風格的casts。
2. 使用cast會產生運行時代碼——不要認為你以為的就是你以為的許多程序員認為cast除了告訴編譯器需要將一個類型當作另外一個類型之外,沒有做任何事情,但這個一個誤區。任何種類的類型轉換(不管顯示cast還是隱式轉換)都會產生運行時代碼。舉個例子:
1 int x, y; 2 ... 3 double d = static_cast<double>(x)/y; // divide x by y, but use 4 // floating point division
將int x轉換成double肯定會產生代碼,因為在大多數系統架構中,int的底層表示同double是不一樣的。這也許不會讓你吃驚,但下面的例子可能亮瞎你的雙眼:
1 class Base { ... }; 2 class Derived: public Base { ... }; 3 Derived d; 4 5 Base *pb = &d; // implicitly convert Derived* ⇒ Base*
這裡我們只是創建了一個指向派生類對象的基類指針,但有時候,這兩個指針(Derived*和Base*)值將會不一樣。在上面的情況中,運行時會在Derived*指針上應用一個偏移量來產生正確的Base*指針值。
最後這個例子表明一個對象(比如Derived類型的對象)可能有多於一個的地址(比如,當Base*指針指向這個對象和Derived*指向這個對象時有兩個地址)。這在C,java和C#中不可能發生。事實上,當使用多繼承時,這種情況總會發生,但在單繼承中也能發生。這意味著在C++中你應該避免對一些東西是如何布局的做出假設。例如,將對象地址轉換成char*指針然後在此指針上面進行指針算術運算幾乎總是會產生未定義行為。
但是注意我說過偏移量“有時候“是需要的。對象的布局方式和地址被計算的方式會隨編譯器的不同而不同。這意味著僅僅因為你了解一種平台上的布局和轉型並不意味著在別的平台上也能如此工作。世界上充滿了從中吸取教訓的悲哀的程序員。
3. Cast很容易被誤用——無效狀態是如何產生的
關於cast的一件有趣的事情是容易寫出看上去正確但實際錯誤的代碼。比如,許多應用框架需要派生類中的虛函數實現首先要調用基類部分。假設我們有一個Window基類和一個SpecialWindow派生類,兩個類中都定義了onResize虛函數。進一步假設SpecialWindow的onResize函數首先要調用Window的onResize函數。下面的實現方式看上去正確,實際上並非如此:
1 class Window { // base class 2 3 public: 4 5 6 7 virtual void onResize() { ... } // base onResize impl 8 9 ... 10 11 }; 12 13 14 15 class SpecialWindow: public Window { // derived class 16 17 public: 18 19 virtual void onResize() { // derived onResize impl; 20 21 static_cast<Window>(*this).onResize(); // cast *this to Window, 22 23 24 // then call its onResize; 25 // this doesn’t work! 26 ... // do SpecialWindow- 27 } // specific stuff 28 ... 29 30 31 };
我已經對代碼中的cast標注了紅色。(它是新風格的cast,使用舊風格的轉換也不會改變如下事實)。正如你所期望的,代碼將*this轉換成一個window對象。因此調用onResize時會觸發Window:: onResize。你可能想不到的是它並沒有在當前的對象上觸發相應的函數。相反,轉型動作為*this的基類部分創建了一份新的臨時拷貝,onResize是在這份拷貝上被觸發的!上面的代碼沒有在當前對象上調用Window::onResize然後在此對象上執行SpecialWindow的指定動作——它在執行特定動作之前,在當前對象基類部分的拷貝之上調用了Window::onResize。如果Window::onResize修改了當前對象(很有可能,既然onResize是non-const成員函數),當前的對象(Window對象)是不會被修改的。修改的是對象的拷貝。然而如果SpecialWIndow::onResize修改當前對象,當前對象將會被修改,導致上面代碼會為當前對象留下一個無效狀態:基類部分沒有被修改,派生類部分卻被修改了。
解決方法是消除cast的使用,你不想欺騙編譯器讓其把*this當作一個基類對象。你想的是在當前對象上調用onResize的基類版本。所以按照下面的方法做:
1 class SpecialWindow: public Window { 2 public: 3 virtual void onResize() { 4 Window::onResize(); // call Window::onResize 5 ... // on *this 6 } 7 ... 8 };
這個例子同樣表明如果你發現你自己想使用cast了,它就標志著你可能會使用錯誤的方式來應用它。使用dynamic_cast的時候也是如此。
4. Dynamic_cast 分析 4.1 Dynamic_cast速度很慢在深入研究dynamic_cast的設計含義之前,我們能觀察到dynamic_cast的很多實現其速度是非常慢的。舉個例子,至少有一種普通的實現在某種程度上是基於類名稱的字符串比較。如果你正在一個4層深的單繼承體系的對象上執行dynamic_cast,在這樣一種實現(也就是上面說的普通實現)下每個dynamic_cast至多可能調用四次strcmp來比較類名稱。一個層次更深的繼承或者一個多繼承可能開銷會更大。這樣實現是有原因的(它們必須支持動態鏈接(dynamic linking))。因此,除了要對使用cast時的一般問題保持機敏,在對性能敏感的代碼中更要對dynamic_cast的使用保持機敏。
4.2 Dynamic_cast的兩種替代方案你需要dynamic_cast是因為你想在你堅信其是派生類對象之上執行派生類操作,但你只能通過基類指針或基類引用來操作此對象。有兩種普通的方法避免使用dynamic_cast
第一, 使用容器直接存儲派生類對象指針(通常情況下使用智能指針,見Item 13),這樣就消除了通過基類接口來操縱這些對象的可能。舉個例子,在我們的window/SpecialWindow繼承體系中,只有SpecialWindows支持blink,不要像下面這樣做:
1 class Window { ... }; 2 class SpecialWindow: public Window { 3 public: 4 void blink(); 5 ... 6 }; 7 8 typedef // see Item 13 for info 9 10 std::vector<std::tr1::shared_ptr<Window> > VPW; // on tr1::shared_ptr 11 12 VPW winPtrs; 13 14 ... 15 16 for (VPW::iterator iter = winPtrs.begin(); // undesirable code: 17 18 iter != winPtrs.end(); // uses dynamic_cast 19 20 ++iter) { 21 22 if (SpecialWindow *psw = dynamic_cast<SpecialWindow*>(iter->get())) 23 24 psw->blink(); 25 26 }
而是用下面的做法:
1 typedef std::vector<std::tr1::shared_ptr<SpecialWindow> > VPSW; 2 3 VPSW winPtrs; 4 5 ... 6 7 8 for (VPSW::iterator iter = winPtrs.begin(); // better code: uses 9 iter != winPtrs.end(); // no dynamic_cast 10 ++iter) 11 (*iter)->blink();
當然這種方法不允許你在同一個容器中存儲所有可能的Window派生物。要達到這個目的,你可能需要多個類型安全的容器。
第二, 在基類中提供虛函數。舉個例子,雖然只有SecialWindos支持blink,你同樣可以在基類中聲明一個blink,但默認實現是什麼都不做:
1 class Window { 2 public: 3 virtual void blink() {} // default impl is no-op; 4 ... // see Item 34 for why 5 }; // a default impl may be 6 // a bad idea 7 class SpecialWindow: public Window { 8 public: 9 virtual void blink() { ... } // in this class, blink 10 11 ... 12 13 // does something 14 15 }; 16 17 18 19 typedef std::vector<std::tr1::shared_ptr<Window> > VPW; 20 21 22 23 24 VPW winPtrs; // container holds 25 // (ptrs to) all possible 26 ... // Window types 27 for (VPW::iterator iter = winPtrs.begin(); 28 iter != winPtrs.end(); 29 ++iter) // note lack of 30 (*iter)->blink(); // dynamic_cast
上面的兩種方法不是在任何情況下都能使用,但是在許多情況下,它們為dynamic_cast提供了一種可行的替代方案。當他們確實能做到你想要的,你應該擁抱它們。
4.3 不要在級聯設計中使用dynamic_cast你絕對想避免的一件事是不要做包含級聯dynamic_cast的設計,也就是像下面這個樣子:
1 class Window { ... }; 2 3 ... 4 5 // derived classes are defined here 6 7 typedef std::vector<std::tr1::shared_ptr<Window> > VPW; 8 9 10 11 VPW winPtrs; 12 13 14 15 ... 16 17 18 19 for (VPW::iterator iter = winPtrs.begin(); iter != winPtrs.end(); ++iter) 20 21 22 23 { 24 25 26 27 if (SpecialWindow1 *psw1 = 28 29 30 31 dynamic_cast<SpecialWindow1*>(iter->get())) { ... } 32 33 34 35 else if (SpecialWindow2 *psw2 = 36 37 38 39 dynamic_cast<SpecialWindow2*>(iter->get())) { ... } 40 41 42 43 else if (SpecialWindow3 *psw3 = 44 45 46 47 dynamic_cast<SpecialWindow3*>(iter->get())) { ... } 48 49 50 51 ... 52 53 54 55 }
這種實現產生的代碼既大又慢,也很脆弱,因為每次Windos類體系發生變化,你都需要為上面的代碼做一次檢查是否需要更新。(例如,如果添加了一個新的派生類,上面的代碼可能需要添加一個新的條件分支)。這樣的代碼應該被基於虛函數的設計替換掉。
5. 把對cast的使用隱藏在函數接口中
好的C++ 代碼很少使用casts,但完全去除它們也是不切實際的。Int 轉換成double這樣的cast是合理的應用,雖然有可能不是必須的。(可以重新聲明一個新的double變量,用x的值來對其進行初始化)。像許多可能令人起疑的設計一樣,要盡可能的對cast的使用進行隔離,可以將其隱藏在調用者看不見的接口中。
6. 總結: