Effective C++ 55條款及應該記住的事項
阿新 • • 發佈:2019-01-02
1. 讓自己習慣C++
01. 視C++為一個語言聯邦
請記住: C++高效程式設計守則視狀況而變化,取決於你使用C++的哪一部分
02. 儘量以const,enum,inline,替換#define
請記住: 對於單純變數,最好以const物件或enums替換#defines
對於形似函式的巨集(macros),最好改用inline函式替換#defines
03. 儘可能使用const
請記住: 將某些東西宣告為const可幫助編譯器偵測出錯誤用法。const可被施加於任何作用域內的物件、函式引數、函式返回型別、成員函式本體。
編譯器強制實施bitwise constness,但你編寫程式時應該使用“概念上的常量性”(conceptual constness)
當const和non-const成員函式有著實質等價的實現時,令non-const版本呼叫const版本可避免程式碼重複
04. 確定物件被使用前已先被初始化
為內建型物件進行手工初始化,因為C++不保證初始化它們。
建構函式最好使用成員初始化列表(member initialization list),而不要在建構函式本體內使用賦值操作(assignment)。初始化列表列出的成員變數,其排列次序應該和它們在class中宣告次序相同。
為免除“跨編譯單元之初始化次序”問題,請以local static物件替換non-local static物件
2. 構造/析構/賦值運算
05. 瞭解C++默默編寫並呼叫哪些函式
請記住: 編譯器可以暗自為class建立default建構函式、copy建構函式、copy assignment操作符,以及解構函式
06. 若不想使用編譯器自動生成的函式,就該明確拒絕
為駁回編譯器自動(暗自)提供的機能,可將相應的成員函式宣告為private並且不予實現。使用像Uncopyable這樣的base class也是一種做法。
07. 為多型基類宣告virtual解構函式
請記住: Polymorphic(帶多型性質的)base classes應該宣告一個virtual解構函式。如果class帶有任何virtual函式,它就應該擁有一個virtual解構函式。
Classes的設計目的如果不是作為base classes使用,或不是為了具備多型性(polymorphically),就不該宣告virtual解構函式。
08. 別讓異常逃離解構函式
請記住: 解構函式絕對不要丟擲異常。如果一個被解構函式呼叫的函式可能丟擲異常,解構函式應該捕捉任何異常,然後吞下它們(不傳播)或結束程式。
如果客戶需要對某個操作函式執行期間丟擲的異常做出反應,那麼class應該提供一個普通函式(而非在解構函式中)執行該操作。
09. 絕不在構造和析構過程中呼叫virtual函式
在構造和析構期間不要呼叫virtual函式,因為這類呼叫從不下降至derived class(比起當前執行建構函式和解構函式的那層)
10. 令operator= 返回一個reference to *this
請記住: 令賦值(assignment)操作符返回一個reference to *this。
11. 在operator= 中處理“自我賦值”
請記住: 確保當物件自我賦值時operator=有良好的行為。其中技術包括比較“來源物件”和“目標物件”的地址、精心周到的語句順序、以及copy-and-swap。
確定任何函式如果操作一個以上的物件,而其中多個物件是同一個物件時,其行為仍然正確。
12. 複製物件時勿忘其每一個成分
請記住: Copying函式應該確保複製“物件內的所有成員變數”及“所有base class成分”。
不要嘗試以某個copying函式實現另一個copying函式。應該將共同機能放進第三個函式中,並由兩個copying函式共同呼叫。
3. 資源管理
13. 以物件管理資源
請記住: 為防止資源洩漏,請使用RAII物件,它們在建構函式中獲得資源並在解構函式中釋放資源。
兩個常被使用的RAII classes分別是tr1::shared_ptr和auto_ptr。前者通常是較佳選擇,因為其copy行為比較直觀。若選擇auto_ptr,複製動作會使它(被複制物)指向null。
14. 在資源管理類中小心copying行為
請記住: 複製RAII物件必須一併複製它所管理的資源,所以資源的copying行為決定RAII物件的copying行為。
普遍而常見的RAII class copying行為是:抑制copying、施行引用計數法(reference counting)。不過其他行為也都可能被實現。
15. 在資源管理類中提供對原始資源的訪問
請記住: APIs往往要求訪問原始資源(raw resources),所以每一個RAII class應該提供一個“取得其所管理之資源”的辦法。
對原始資源的訪問可能經由顯示轉換或隱式轉換。一般而言顯示轉換比較安全,但隱式轉換對客戶比較方便。
16. 成對使用new和delete時要採用相同形式
請記住: 如果你在new表示式中使用[],必須在相應的delete表示式中也使用[]。如果你在new表示式中不使用[],一定不要在相應的delete表示式中使用[]。
17. 以獨立語句將newed物件置入智慧指標
請記住: 以獨立語句將newed物件儲存於(置入)智慧指標內。如果不這樣做,一旦異常被丟擲,有可能導致難以察覺的資源洩漏。
4. 設計與宣告
18. 讓介面容易被正確使用,不易被誤用
請記住: 好的介面很容易被正確使用,不容易被誤用。你應該在你的所有介面中努力達成這些性質。
”促進正確使用“的方法包括介面的一致性,以及與內建型別的行為相容。
”阻止誤用“的辦法包括建立新型別、限制類型上的操作,束縛物件值,以及消除客戶的資源管理責任。
tr1::shared_ptr支援定製型刪除器(custom deleter)。這可防範DLL問題,可被用來自動解除互斥鎖(mutexes;見條款14)等等。
19. 設計class猶如設計type
請記住: Class的設計就是type的設計。在定義一個新type之前,請確定你已經考慮過本條款覆蓋的所有討論主題。
20. 寧以pass-by-reference-to-const替代pass-by-value
請記住: 儘量以pass-by-reference-to-const替換pass-by-value。前者通常比較高效,並可避免切割問題(slicing problem)。
以上規則並不適用於內建型別,以及STL的迭代器和函式物件。對它們而言,pass-by-value往往比較適當。
21. 必須返回物件時,別妄想返回其reference
請記住: 絕不要返回pointer或reference指向一個local stack物件,或返回reference指向一個heap-allocated物件,或者返回pointer或reference指向一個local static物件而有可能同時需要多個這樣的物件。條款4已經為”在單執行緒環境中合理返回reference指向一個local static物件“提供了一份設計例項。
22. 將成員變數宣告為private
請記住: 切記將成員變數宣告為private。這可賦予客戶訪問資料的一致性、可細微劃分訪問控制、允諾約束條件獲得保證,並提供class作者以充分的實現彈性。
Protected並不比public更具封裝性
23. 寧以non-member,non-friend替換member函式
請記住: 寧可拿non-member non-friend函式替換member函式。這樣做可以增加封裝性、包裹彈性(packaging flexibility)和機能擴充性。
24. 若所有引數皆需型別轉換,請為此採用non-member函式
請記住: 如果你需要為某個函式的所有引數(包括被this指標所指的那個隱喻引數)進行型別轉換,那麼這個函式必須是個non-member。
25. 考慮寫出一個不拋異常的swap函式
請記住: 當std::swap對你的型別效率不高時,提供一個swap成員函式,並確定這個函式不丟擲異常。
如果你提供一個member swap,也該提供一個non-member swap用來呼叫前者。對於classes(而非templates),也請特化std::swap。
呼叫swap時應針對std::swap使用using宣告式,然後呼叫swap並且不帶任何“名稱空間資格修飾”
為“使用者定義型別”進行std templates全特化是好的,但千萬不要嘗試在std內加入某些對std而言全新的東西。
5. 實現
26. 儘可能延後變數定義式的出現時間
請記住: 儘可能延後變數定義式的出現。這樣做可增加程式的清晰度並改善程式效率。
27. 儘量少做轉型動作
請記住: 如果可以,儘量避免轉型,特別是在注重效率的程式碼中避免dynamic_casts。如果有個設計需要轉型動作,試著發展無需轉型的替代設計。
如果轉型是必要的,試著將它隱藏於某個函式背後。客戶隨後可以呼叫該函式,而不需將轉型放進他們自己的程式碼內。
寧可使用C++-style(新式)轉型,不要使用舊式轉型。前者很容易辨識出來,而且也比較有著分門別類的職掌。
28. 避免返回handles指向物件內部成分
請記住: 避免返回handles(包括references、指標、迭代器)指向物件內部。遵守這個條款可增加封裝性,幫助const成員函式的行為像個const,並將發生“虛吊號碼牌”(dangling handles)的可能性降至最低。
29. 為“異常安全”而努力是值得的
請記住: 異常安全函式(Exception-safe functions)即使發生異常也不會洩漏資源或允許任何資料結構敗壞。這樣的函式區分為三種可能的保證:基本型、強烈型、不拋異常型。
“強烈保證”往往能夠以copy-and-swap實現出來,但“強烈保證”並非對所有函式都可實現或具備現實意義。
函式提供的“異常安全保證”通常最高只等於其所呼叫之各個函式的“異常安全保證”中的最弱者。
30. 透徹瞭解inlining的裡裡外外
請記住: 將大多數inlining限制在小型、被頻繁呼叫的函式身上。這可使日後的除錯過程和二進位制升級更容易,也可使潛在的程式碼膨脹問題最小化,使程式的速度提升機會最大化。
不要只因為function templates出現在標頭檔案,就將它們宣告為inline。
31. 將檔案間的編譯依存關係降至最低
請記住: 支援“編譯依存性最小化”的一般構想是:相依於宣告式,不要相依於定義式。基於此構想的兩個手段是Handles calsses和Interface classes。
程式庫標頭檔案應該以“完全且僅有宣告式”(full and declaration-only forms)的形式存在。這種做法不論是否涉及templates都適用。
6. 繼承與面向物件設計
32. 確定你的public繼承塑模出is-a關係
請記住: ”public繼承“意味is-a。適用於base calsses身上的每一件事情一定也適用於derived classes身上,因為每一個derived class物件也都是一個base class物件。
33. 避免遮掩繼承而來的名稱
請記住: derived classes內的名稱會遮掩base classes內的名稱。在public繼承下從來沒有人希望如此。
為了讓被遮掩的名稱再見天日,可使用using宣告式或轉交函式(forwarding functions)
34. 區分介面繼承和實現繼承
請記住: 介面繼承和實現繼承不同。在public繼承之下,derived classes總是繼承base class的介面。
pure virtual函式只具體指定介面繼承。
簡樸的(非純)impure virtual函式具體指定介面繼承及預設實現繼承。
non-virtual函式具體指定介面繼承以及強制性實現繼承。
35. 考慮virtual函式以外的其他選擇
請記住: virtual函式的替代方案包括NVI手法及Strategy設計模式的多種形式。NVI手法自身是一個特殊形式的Template Method設計模式。
將機能從成員函式移到class外部函式,帶來的一個缺點是,非成員函式無法訪問class的non-public成員。
tr1::function物件的行為就像一般函式指標。這樣的物件可接納”與給定之目標籤名式(target signature)相容“的所有可呼叫物(callable entities)。
36. 絕不重新定義繼承而來的non-virtual函式
請記住: 絕對不要重新定義繼承而來的non-virtual函式。
37. 絕不重新定義繼承而來的預設引數值
請記住: 絕對不要重新定義一個繼承而來的預設引數值,因為預設引數值都是靜態繫結,而virtual函式——你唯一應該覆寫的東西——確實動態繫結。
38. 通過複合塑模出has-a或“根據某物實現出”
請記住: 複合(composition)的意義和public繼承完全不同
在應用域(application domain),複合意味has-a(有一個)。在實現域(implementation domain),複合意味is-implemented-in-terms-of(根據某物實現出)。
39. 明智而審慎地使用private繼承
請記住: private繼承意味著is-implemented-in-terms of(根據某物實現出)。它通常比複合的級別低。但是當derived class需要訪問protected base class的成員,或需要重新定義繼承而來的virtual函式時,這麼設計是合理的。
和複合(composition)不同,private繼承可以造成empty base最優化。這對致力於“物件尺寸最小化”的程式庫開發者而言,可能很重要。
40. 明智而審慎地使用多重繼承
請記住: 多重繼承比單一繼承複雜。它可能導致新的歧異性,以及對virtual繼承的需要。
Virtual繼承會增加大小、速度、初始化(及賦值)複雜度等等成本。如果virtual base classes不帶任何資料,將是最具實用價值的情況。
多重繼承的確有正當用途。其中一個情節涉及“public繼承某個Interface class”和“private繼承某個協助實現的class”的兩相組合。
7. 模板與泛型程式設計
41. 瞭解隱式介面和編譯期多型
請記住: classes和templates都支援介面(interfaces)和多型(polymorphism)。
對classes而言介面是顯式的(explicit),以函式簽名為中心。多型則是通過virtual函式發生於執行期。
對template引數而言,介面是隱式的(implicit),奠基於有效表示式。多型則是通過template具現化和函式過載解析(function overloading resolution)發生於編譯期。
42. 瞭解typename的雙重意義
請記住: 宣告template引數時,字首關鍵字class和typename可互換。
請使用關鍵字typename標識巢狀從屬型別名稱;但不得在base class lists(基類列)或member initialization list(成員初值列)內以它作為base class修飾符。
43. 學習處理模板化基類內的名稱
請記住: 可在derived class templates內通過“this->”指涉base class templates內的成員名稱,或藉由一個明白寫出的“base class資格修飾符”完成。
44. 將與引數無關的程式碼抽離templates
請記住: Templates生成多個classes和多個函式,所以任何template程式碼都不該與某個造成膨脹的template引數產生相依關係。
因非型別模板引數(non-type template parameters)而造成的程式碼膨脹,往往可消除,做法是以函式引數或class成員變數替換template引數。
因型別引數(type parameter)而造成的程式碼膨脹,往往可降低,做法是讓帶有完全相同二進位制表述(binary representations)的具體型別(instantiation types)共享實現碼。
45. 運用成員函式模板接受所有相容型別
請記住: 請使用member function templates(成員函式模板)生成“可接受所有相容型別”的函式。
如果你宣告member templates用於“泛化copy構造”或“泛化assignment操作”,你還是需要宣告正常的copy建構函式和copy assignment操作符。
46. 需要型別轉換時請為模板定義非成員函式
請記住: 當我們編寫一個calss template,而它所提供之“與此template相關的”函式支援“所有引數之隱式型別轉換”時,請將那些函式定義為“class template內部的friend函式”。
47. 請使用traits classes變現型別資訊
請記住: Traits classes使得“型別相關資訊”在編譯期可用。它們以templates和“templates特化”完成實現。
整合過載技術(overloading)後,traits classes有可能在編譯期對型別執行if…else測試。
48. 認識template超程式設計
請記住: Template metaprogramming(TMP,模板超程式設計)可將工作由執行期移往編譯期,因為得以實現早起錯誤偵測和更高的執行效率。
TMP可被用來生成“基於政策選擇組合”(based on combinations of policy choices)的客戶定製程式碼,也可用來避免生成對某些特殊型別並不合適的程式碼。
8. 定製new和delete
49. 瞭解new-handler的行為
請記住: set_new_handler允許客戶指定一個函式,在記憶體分配無法滿足時被呼叫。
Nothrow new是一個頗為侷限的工具,因為它只適用於記憶體分配;後繼的建構函式呼叫還是可能丟擲異常。
50. 瞭解new和delete的合理替換實際
請記住: 有許多理由需要寫個自定的new和delete,包括改善效能、對heap運用錯誤進行除錯、收集heap使用資訊。
51. 編寫new和delete時需固守常規
請記住: operation new應該內含一個無窮迴圈,並在其中嘗試分配記憶體,如果它無法滿足記憶體需求,就該呼叫new-handler。它也應該有能力處理0bytes申請。Class專屬版本則還應該處理“比正確大小更大的(錯誤)申請”。
operation delete應該在收到null指標時不做任何事。Class專屬版本則還應該處理“比正確大小更大的(錯誤)申請”。
52. 寫了placement new也要寫placement delete
請記住: 當你寫一個placement operator new,請確定也寫出了對應的placement operator delete。如果沒有這樣做,你的程式可能會發生隱微而時斷時續的記憶體洩漏。
當你宣告placement new和placement delete,請確定不要無意識(非故意)地遮掩了它們的正常版本。
9. 雜項討論
53. 不要輕忽編譯器的警告
請記住: 嚴肅對待編譯器發出的警告資訊。努力在你的編譯器的最高(最嚴苛)警告級別下爭取“無任何警告”的榮譽。
不要多度依賴編譯器的報警能力,因為不同的編譯器對待事情的態度並不相同。一旦移植到另一編譯器上,你原來依賴的警告資訊有可能消失。
54. 讓自己熟悉包括TR1在內的標準程式庫
請記住: C++標準程式庫的主要機能由STL、iostreams、locales組成。幷包含C99標準程式庫。
TR1添加了智慧指標(例如tr1::shared_ptr)、一般化函式指標(tr1::function)、hash-based容器、正則表示式(regular expression)以及另外10個元件的支援。
TR1自身只是一份規範。為獲得TR1提供的好處,你需要一份實物。一個好的實物來源是Boost。
55. 讓自己熟悉Boost
請記住: Boost是一個社群,也是一個網站。致力於免費、原始碼開放、同僚複審的C++程式庫開發。Boost在C++標準化過程中扮演深具影響力的角色。
Boost提供許多TR1元件實現品,以及其他許多程式庫。