改善程式與設計的55個具體做法
1. 視C++為一個語言聯邦
- C++並不是一個帶有一組守則的一體語言:它是從四個次語言(C, Object-Oriented C++, Template C++ 以及 STL)組成的聯邦政府,每個語言都有自己的規約。
- C++的高效程式設計守則視狀況而變化,取決於你使用C++的哪一部分
2. 儘量以const, enum, inline替換#define
- 即寧可以編譯器替換前處理器
- 對於單純常量,最好以const物件或enums替換#define
- 對於形似函式的巨集,最好改用inline函式替換#define
3. 儘可能使用const
- 將某些東西宣告為const可幫助編譯器偵測出錯誤用法。const可被施加於任何作用域內的物件、函式引數、函式返回型別、成員函式本體
- 編譯器強制實施bitwise constness,但你編寫程式時應該使用“概念上的常量性”
- 當const和non-const成員函式有著實質等價的實現時,令non-const版本呼叫const版本可避免程式碼重複
4.確定物件被使用前已被初始化
- 為內建型物件進行手工初始化,因為C++不保證初始化它們
- 建構函式最好使用成員初始值列,而不要在建構函式本體內使用賦值操作。初值列列出的成員變數,其排列次序應該和它們在class中的宣告次序相同
- 為免除“跨編譯單元之初始化次序”問題,請以local static物件替換non-local static物件
5. 瞭解C++默默編寫並呼叫了哪些函式
- 編譯器可以暗自為class建立default建構函式、copy建構函式、copy assignment操作符,以及解構函式
6. 如不想使用編譯器自動生成的函式,就該明確拒絕
- 為駁回編譯器自動提供的機能,可將相應的成員函式宣告為private並且不予實現
7. 為多型基類宣告virtual解構函式
- 帶有多型性質的基類應該宣告一個virtual解構函式。如果class帶有任何virtual函式,它就應該擁有一個virtual解構函式
- 類的設計如果不是作為基類使用,或不是為了具備多型性,就不該宣告virtual解構函式
8. 別讓異常逃離解構函式
- 只要解構函式突出異常,程式就可能過早結束或出現不明確行為
- 解構函式絕對不要吐出異常。如果一個被解構函式呼叫的函式可能丟擲異常,解構函式應該捕獲任何異常,然後吞下它們(不傳播)或者結束程式
- 如果客戶需要對某個操作函式執行期間丟擲的異常做出反應,那麼class應該提供一個普通函式(而非在解構函式中)執行該操作
9. 絕不在構造和析構過程中呼叫virtual函式
- 在構造和析構期間不要呼叫virtual函式,因為這類呼叫從不下降至派生類
10. 令operator=返回一個reference to *this
- 為了實現連鎖賦值
11. 在operator=中處理“自我賦值”
- 確保物件自我賦值時operator=有良好行為。其中技術包括“來源物件”和“目標物件”的地址、精心周到的語句順序以及copy-and-swap
- 確定任何函式如果操作一個以上物件,而其中多個物件是同一個物件時,其行為仍然正確
12. 複製物件時勿忘其每一個成分
- Copying函式應該確保複製“物件內的所有成員變數”以及“所有base class成分”
- 不要嘗試以某個copying函式實現另一個copying函式。應該將共同機能放進第三個函式中,並由兩個copying函式共同呼叫
13. 以物件管理資源
- 獲得資源後立刻放進管理物件
- 管理物件運用解構函式確保資源被釋放
- 為防止資源洩露,請使用RAII物件,它們在建構函式中獲得資源並在解構函式中釋放資源
- 兩個常被使用的RAII classes分別是tr1::shared_ptr和auto_ptr。前者通常是較佳選擇,因為其copy行為比較直觀。若選擇auto_ptr,複製動作會使它(被複制物)指向null
14. 在資源管理類中小心copying行為
- 複製RAII物件必須一併複製它所管理的資源,所以資源的copying行為決定RAII物件的copying行為
- 普遍而常見的RAII class copying行為是:抑制copying、施行引用計數法。不過其他行為也都可能被實現
15. 在資源管理類中提供對原始資源的訪問
- APIs往往要求訪問原始資源,所以每一個RAII class應該提供一個“取得其所管理的資源”的辦法
- 對原始資源的訪問可能經由顯示轉換或隱式轉換。一般而言顯示轉換比較安全,但隱式轉換對客戶比較方便
16. 成對使用new和delete時,要採取相同形式
- 如果你在new表示式中使用[],必須在相應的delete表示式中也使用[]。如果在new表示式中不使用,在delete中也不要使用
17. 以獨立語句將new物件置入智慧指標
- 如果不這樣做,一旦異常被丟擲,有可能導致難以察覺的資源洩露
18. 讓介面容易被正確使用,不易被誤用
- 好的介面很容易被正確使用,不容易被誤用。你應該在你的所有介面中努力達成在這些性質
- “促進正確使用”的辦法包括介面的一致性,以及與內建型別的行為相容
- “阻止誤用”的辦法包括建立新型別、限制型別上的操作,束縛物件值,以及消除客戶的資源管理責任
- tr1::shared_ptr支援定製型刪除器。這可防範DLL問題,可被用來自動解除互斥鎖等等
19. 設計class猶如設計type
20. 寧以pass-by-reference-to-const替換pass-by-value
- 前者通常比較高效,並可避免切割問題
- 該規則並不適用內建型別,以及STL的迭代器和函式物件。對它們而言,pass-by-value往往比較適當
21. 必須返回物件時,別妄想返回其reference
22. 將成員變數宣告為private
- 這可賦予客戶訪問資料的一致性、可細微劃分訪問控制,允諾約束條件獲得保證,並提供class作者以充分的實現彈性
- protected並不比public更具有封裝性
23. 寧以non-member、non-friend替換member函式
- 這樣做可以增加封裝性、包裹彈性和機能擴充性
24. 若所有引數皆需型別轉換,請為此採用non-member函式
- 即,如果你需要為某個函式(包括被this指標所指的那個隱喻引數)進行型別轉換,那麼這個函式必須是個non-member
25. 考慮寫出一個不丟擲異常的swap函式
- 當std:swap對你的型別效率不高時,提供一個swap成員函式,並確定這個函式不丟擲異常
- 如果你提供一個member swap,也該提供一個non-member swap用來呼叫前者。對於classes,也請特化std:swap
- 呼叫swap時應針對std::swap使用using宣告式,然後呼叫swap並且不帶任何“名稱空間資格修飾”
- 為“使用者定義型別”進行std templates全特化是好的,但千萬不要嘗試在std內加入某些對std而言全新的東西
26. 儘可能延後變數定義式的出現時間
- 這樣可增加程式的清晰度並改善程式效率
27. 儘量少做轉型動作
- const_cast通常被用來將物件的常量性剔除
- dynamic_cast主要用來執行“安全向下轉型”,也就是用來判斷某物件是否歸屬繼承體系中的某個型別
- reinterpret_cast意圖執行低階轉型,實際動作及結果可能取決於編譯器,這也就表示它不可移植
- static_cast用來強迫隱式轉換,但它無法將const轉為non-const
- 如果可以,儘量避免轉型,特別是在注重效率的程式碼中避免dynamic_casts。如果有個設計需要轉型動作,試著發展無需轉型的替代設計
- 如果轉型是必要的,試著將它隱藏於某個函式背後。客戶可以呼叫該函式,而不需要將轉型放進他們自己的程式碼內
- 寧可以C++-style轉型,不要使用舊式轉型
28. 避免返回handles指向物件內部成分
- 該條款可增加封裝性,幫助const成員函式的行為像個const,並將發生 dangling handles的可能性降至最低
29. 為“異常安全”而努力是值得的
- 異常安全函式提供一下三個保證之一
- 基本承諾:如果異常被丟擲,程式內的任何事物仍然保持在有效狀態下
- 強烈保證:如果異常被丟擲,程式狀態不改變
- 不拋擲保證:承諾絕不丟擲異常,因為它們總是能夠完成它們原先承諾的功能
30. 徹底瞭解inlining的裡裡外外
- 將inlining限制在小型、被頻繁呼叫的函式身上
- 不要只因為function templates出現在標頭檔案,就將它們宣告為inline
31. 將檔案間的編譯依存關係降至最低
- 如果使用object references或object pointers可以完成任務,就不要使用objects
- 如果能夠,儘量以class宣告式替換class定義式
- 為宣告式和定義式提供不同的標頭檔案
- 程式庫標頭檔案應該以“完全且僅有宣告式”的形式存在
32. 確定你的public繼承塑模出is-a關係
33. 避免遮掩繼承而來的名稱
- 派生類內的名稱會遮掩基類內的名稱。在public繼承下從沒有人希望如此
- 為了讓被遮掩的名稱再見天日,可使用using宣告式或轉交函式
34. 區分介面繼承和實現繼承
- 介面繼承和實現繼承不同。在public繼承之下,派生類總是繼承基類的介面
- pure virtual函式只具體指定介面繼承
- impure virtual函式具體指定介面繼承及預設實現繼承
- non-virtual函式具體指定介面繼承以及強制性實現繼承
35. 考慮virtual函式以外的其他選擇
- virtual函式的替代方案包括NVI手法以及Strategy設計模式的多種形式。NVI手法自身是一個特殊形式的Template Method設計模式
- 將機能從成員函式轉移到class外部函式,帶來的一個缺點是,非成員函式無法訪問class的non-public成員
- tr1::function物件的行為就像一般函式指標。這樣的物件可接納“與給定的目標標籤格式相容”的所有可呼叫物
36. 絕不重新定義繼承而來的non-virtual函式
37. 絕不重新定義繼承而來的預設引數值
- 因為預設數值都是靜態繫結,而virtual函式確實動態繫結
38. 通過複合塑模出has-a或“根據某物實現出”
39. 明智而審慎地使用private繼承
- private繼承意味著is-implemented-in-terms-of(根據某物實現出)。它通常比複合的級別低。但是當派生類需要訪問受保護的基類的成員,或需要重新定義繼承而來的virtual函式時,這麼設計是合理的
- 和複合不同,private繼承可以造成empty base最優化。這對致力於“物件尺寸最小化”的程式庫開發者而言,可能很重要
40. 明智而審慎地使用多重繼承
- 多重繼承比單一繼承複雜。它可能導致新的歧義性,以及對virtual繼承的需要
- virtual繼承會增加大小、速度、初始化(及賦值)複雜度等等成本。如果virtual base classes不帶任何資料,將是最具使用價值的情況
- 多重繼承的確有正當用途。其中一個情節涉及“public繼承某個Interface class”和“private繼承某個協助實現的class”的兩相組合
41. 瞭解隱式介面和編譯器多型
- class和templates都支援介面和多型
- 對classes而言介面是顯示的,以函式簽名為中心,多型則是通過virtual函式發生於執行期
- 對template引數而言,介面是隱式的,奠基於有效表示式。多型則是通過template具現化和函式過載解析發生於編譯期
42. 瞭解typename的雙重意義
- 宣告template引數時,字首關鍵字class和typename可互換
- 請使用關鍵字typename標識巢狀從屬型別名稱;但不得在base class lists或member initialization list內以它作為base class修飾符
43. 學習處理模板化基類內的名稱
44. 將與引數無關的程式碼抽離templates
- Templates生成多個classes和多個函式,所以任何template程式碼都不該與某個造成膨脹的template引數產生相依關係
- 因非型別模板引數而造成的程式碼膨脹,往往可消除,做法是以函式引數或class成員變數替換template引數
- 因型別引數而造成的程式碼膨脹,往往可降低,做法是讓帶有完全相同二進位制表述的具現型別共享實現碼
45. 運用成員函式模板接受所有相容型別
- 如果你宣告member templates用於泛化copy構造或泛化assignment操作,你還需要宣告正常的copy建構函式和copy assignment操作符
46. 需要型別轉換時請為模板定義非成員函式
- 當我們編寫一個class template,而它所提供的“與此template相關的”函式支援“所有引數的隱式型別轉換”時,請將那些函式定義為“class template內部的friend函式”
47. 請使用traits classes表現型別資訊
- Traits classes使得“型別相關資訊”在編譯器可用。它們以templates和“templates特化”完成實現
- 整合過載技術後,traits classes有可能在編譯器對型別執行if...else測試
48. 認識template超程式設計
- Template metaprogramming (TMP,模板超程式設計)可將工作由執行期移往編譯期,因而得以實現早期錯誤偵測和更高的執行效率
- TMP可被用來生成“基於政策選擇組合”的客戶定製程式碼,也可用來避免生成對某些特殊型別並不適合的程式碼
49.瞭解new-handler的行為
- set_new_handler允許客戶指定一個函式,在記憶體分配無法獲得滿足時被呼叫
- Nothrow new是一個頗為侷限的工具,因為它只適用於記憶體分配;後繼的建構函式呼叫還是可能丟擲異常
50. 瞭解new和delete的合理替換時機
- 有許多理由需要寫個自定的new和delete,包括改善效能、對heap運用錯誤進行除錯、收集heap使用資訊
51. 編寫new和delete時需固守常規
- operator new應該內含一個無窮迴圈,並在其中嘗試分配記憶體,如果它無法滿足記憶體需求,就應該呼叫new-handler。它也應該有能力處理任何0 bytes申請。Class專屬版本則還應該處理“比正確大小更大的(錯誤)申請”
- operator delete應該在收到null指標時不做任何事。Class專屬版本則還應該處理“比正確大小更大的(錯誤)申請”
52. 寫了placement new也要寫placement delete
- 如果沒有這樣做,你的程式可能會發生隱微而時斷時續的記憶體洩露
- 當宣告placement new和placement delete時,不要無意識地遮掩它們的正常版本
53. 不要輕忽編譯器的警告資訊
54.讓自己熟悉包括TR1在內的標準程式庫(Boost)
55. 讓自己熟悉Boost