Effective C++
- 視 C++ 爲一個語言聯邦(C、Object-Oriented C++、Template C++、STL)
- 寧可以編譯器替換預處理器(儘量以 const、enum、inline 替換 #define)
- 儘可能使用 const
- 確定對象被使用前已先被初始化(構造時賦值(copy 構造函數)比 default 構造後賦值(copy assignment)效率高)
- 瞭解 C++ 默默編寫並調用哪些函數(編譯器暗自爲 class 創建 default 構造函數、copy 構造函數、copy assignment 操作符、析構函數)
- 若不想使用編譯器自動生成的函數,就應該明確拒絕(將不想使用的成員函數聲明爲 private,並且不予實現)
- 爲多態基類聲明 virtual 析構函數(如果 class 帶有任何 virtual 函數,它就應該擁有一個 virtual 析構函數)
- 別讓異常逃離析構函數(析構函數應該吞下不傳播異常,或者結束程序,而不是吐出異常;如果要處理異常應該在非析構的普通函數處理)
- 絕不在構造和析構過程中調用 virtual 函數(因爲這類調用從不下降至 derived class)
- 令 operator= 返回一個 reference to *this (用於連鎖賦值)
- 在 operator= 中處理 “自我賦值”
- 賦值對象時應確保複製 “對象內的所有成員變量” 及 “所有 base class 成分”(調用基類複製構造函數)
- 以對象管理資源(資源在構造函數獲得,在析構函數釋放,建議使用智能指針,資源取得時機便是初始化時機(Resource Acquisition Is Initialization,RAII))
- 在資源管理類中小心 copying 行爲(普遍的 RAII class copying 行爲是:抑制 copying、引用計數、深度拷貝、轉移底部資源擁有權(類似 auto_ptr))
- 在資源管理類中提供對原始資源(raw resources)的訪問(對原始資源的訪問可能經過顯式轉換或隱式轉換,一般而言顯示轉換比較安全,隱式轉換對客戶比較方便)
- 成對使用 new 和 delete 時要採取相同形式(new 中使用 [] 則 delete [],new 中不使用 [] 則 delete)
- 以獨立語句將 newed 對象存儲於(置入)智能指針(如果不這樣做,可能會因爲編譯器優化,導致難以察覺的資源泄漏)
- 讓接口容易被正確使用,不易被誤用(促進正常使用的辦法:接口的一致性、內置類型的行爲兼容;阻止誤用的辦法:建立新類型,限制類型上的操作,約束對象值、消除客戶的資源管理責任)
- 設計 class 猶如設計 type,需要考慮對象創建、銷燬、初始化、賦值、值傳遞、合法值、繼承關係、轉換、一般化等等。
- 寧以 pass-by-reference-to-const 替換 pass-by-value (前者通常更高效、避免切割問題(slicing problem),但不適用於內置類型、STL迭代器、函數對象)
- 必須返回對象時,別妄想返回其 reference(絕不返回 pointer 或 reference 指向一個 local stack 對象,或返回 reference 指向一個 heap-allocated 對象,或返回 pointer 或 reference 指向一個 local static 對象而有可能同時需要多個這樣的對象。)
- 將成員變量聲明爲 private(爲了封裝、一致性、對其讀寫精確控制等)
- 寧以 non-member、non-friend 替換 member 函數(可增加封裝性、包裹彈性(packaging flexibility)、機能擴充性)
- 若所有參數(包括被this指針所指的那個隱喻參數)皆須要類型轉換,請爲此採用 non-member 函數
- 考慮寫一個不拋異常的 swap 函數
- 儘可能延後變量定義式的出現時間(可增加程序清晰度並改善程序效率)
- 儘量少做轉型動作(舊式:(T)expression、T(expression);新式:const_cast(expression)、dynamic_cast(expression)、reinterpret_cast(expression)、static_cast(expression)、;儘量避免轉型、注重效率避免 dynamic_casts、儘量設計成無需轉型、可把轉型封裝成函數、寧可用新式轉型)
- 避免使用 handles(包括 引用、指針、迭代器)指向對象內部(以增加封裝性、使 const 成員函數的行爲更像 const、降低 “虛吊號碼牌”(dangling handles,如懸空指針等)的可能性)
- 爲 “異常安全” 而努力是值得的(異常安全函數(Exception-safe functions)即使發生異常也不會泄露資源或允許任何數據結構敗壞,分爲三種可能的保證:基本型、強列型、不拋異常型)
- 透徹瞭解 inlining 的裏裏外外(inlining 在大多數 C++ 程序中是編譯期的行爲;inline 函數是否真正 inline,取決於編譯器;大部分編譯器拒絕太過複雜(如帶有循環或遞歸)的函數 inlining,而所有對 virtual 函數的調用(除非是最平淡無奇的)也都會使 inlining 落空;inline 造成的代碼膨脹可能帶來效率損失;inline 函數無法隨着程序庫的升級而升級)
- 將文件間的編譯依存關係降至最低(如果使用 object references 或 object pointers 可以完成任務,就不要使用 objects;如果能夠,儘量以 class 聲明式替換 class 定義式;爲聲明式和定義式提供不同的頭文件)
- 確定你的 public 繼承塑模出 is-a(是一種)關係(適用於 base classes 身上的每一件事情一定適用於 derived classes 身上,因爲每一個 derived class 對象也都是一個 base class 對象)
- 避免遮掩繼承而來的名字(可使用 using 聲明式或轉交函數(forwarding functions)來讓被遮掩的名字再見天日)
- 區分接口繼承和實現繼承(在 public 繼承之下,derived classes 總是繼承 base class 的接口;pure virtual 函數只具體指定接口繼承;非純 impure virtual 函數具體指定接口繼承及缺省實現繼承;non-virtual 函數具體指定接口繼承以及強制性實現繼承)
- 考慮 virtual 函數以外的其他選擇(如 Template Method 設計模式的 non-virtual interface(NVI)手法,將 virtual 函數替換爲 “函數指針成員變量”,以 tr1::function 成員變量替換 virtual 函數,將繼承體系內的 virtual 函數替換爲另一個繼承體系內的 virtual 函數)
- 絕不重新定義繼承而來的 non-virtual 函數
- 絕不重新定義繼承而來的缺省參數值,因爲缺省參數值是靜態綁定(statically bound),而 virtual 函數卻是動態綁定(dynamically bound)
- 通過複合塑模 has-a(有一個)或 “根據某物實現出”(在應用域(application domain),複合意味 has-a(有一個);在實現域(implementation domain),複合意味着 is-implemented-in-terms-of(根據某物實現出))
- 明智而審慎地使用 private 繼承(private 繼承意味着 is-implemented-in-terms-of(根據某物實現出),儘可能使用複合,當 derived class 需要訪問 protected base class 的成員,或需要重新定義繼承而來的時候 virtual 函數,或需要 empty base 最優化時,才使用 private 繼承)
- 明智而審慎地使用多重繼承(多繼承比單一繼承複雜,可能導致新的歧義性,以及對 virtual 繼承的需要,但確有正當用途,如 “public 繼承某個 interface class” 和 “private 繼承某個協助實現的 class”;virtual 繼承可解決多繼承下菱形繼承的二義性問題,但會增加大小、速度、初始化及賦值的複雜度等等成本)
- 瞭解隱式接口和編譯期多態(class 和 templates 都支持接口(interfaces)和多態(polymorphism);class 的接口是以簽名爲中心的顯式的(explicit),多態則是通過 virtual 函數發生於運行期;template 的接口是奠基於有效表達式的隱式的(implicit),多態則是通過 template 具現化和函數重載解析(function overloading resolution)發生於編譯期)
- 瞭解 typename 的雙重意義(聲明 template 類型參數是,前綴關鍵字 class 和 typename 的意義完全相同;請使用關鍵字 typename 標識嵌套從屬類型名稱,但不得在基類列(base class lists)或成員初值列(member initialization list)內以它作爲 base class 修飾符)
- 學習處理模板化基類內的名稱(可在 derived class templates 內通過 this-> 指涉 base class templates 內的成員名稱,或藉由一個明白寫出的 “base class 資格修飾符” 完成)
- 將與參數無關的代碼抽離 templates(因類型模板參數(non-type template parameters)而造成代碼膨脹往往可以通過函數參數或 class 成員變量替換 template 參數來消除;因類型參數(type parameters)而造成的代碼膨脹往往可以通過讓帶有完全相同二進制表述(binary representations)的實現類型(instantiation types)共享實現碼)
- 運用成員函數模板接受所有兼容類型(請使用成員函數模板(member function templates)生成 “可接受所有兼容類型” 的函數;聲明 member templates 用於 “泛化 copy 構造” 或 “泛化 assignment 操作” 時還需要聲明正常的 copy 構造函數和 copy assignment 操作符)
- 需要類型轉換時請爲模板定義非成員函數(當我們編寫一個 class template,而它所提供之 “與此 template 相關的” 函數支持 “所有參數之隱式類型轉換” 時,請將那些函數定義爲 “class template 內部的 friend 函數”)
- 請使用 traits classes 表現類型信息(traits classes 通過 templates 和 “templates 特化” 使得 “類型相關信息” 在編譯期可用,通過重載技術(overloading)實現在編譯期對類型執行 if…else 測試)
- 認識 template 元編程(模板元編程(TMP,template metaprogramming)可將工作由運行期移往編譯期,因此得以實現早期錯誤偵測和更高的執行效率;TMP 可被用來生成 “給予政策選擇組合”(based on combinations of policy choices)的客戶定製代碼,也可用來避免生成對某些特殊類型並不適合的代碼)
- 瞭解 new-handler 的行爲(set_new_handler 允許客戶指定一個在內存分配無法獲得滿足時被調用的函數;nothrow new 是一個頗具侷限的工具,因爲它只適用於內存分配(operator new),後繼的構造函數調用還是可能拋出異常)
- 瞭解 new 和 delete 的合理替換時機(爲了檢測運用錯誤、收集動態分配內存之使用統計信息、增加分配和歸還速度、降低缺省內存管理器帶來的空間額外開銷、彌補缺省分配器中的非最佳齊位、將相關對象成簇集中、獲得非傳統的行爲)
- 編寫 new 和 delete 時需固守常規(operator new 應該內涵一個無窮循環,並在其中嘗試分配內存,如果它無法滿足內存需求,就應該調用 new-handler,它也應該有能力處理 0 bytes 申請,class 專屬版本則還應該處理 “比正確大小更大的(錯誤)申請”;operator delete 應該在收到 null 指針時不做任何事,class 專屬版本則還應該處理 “比正確大小更大的(錯誤)申請”)
- 寫了 placement new 也要寫 placement delete(當你寫一個 placement operator new,請確定也寫出了對應的 placement operator delete,否則可能會發生隱微而時斷時續的內存泄漏;當你聲明 placement new 和 placement delete,請確定不要無意識(非故意)地遮掩了它們地正常版本)
- 不要輕忽編譯器的警告
- 讓自己熟悉包括 TR1 在內的標準程序庫(TR1,C++ Technical Report 1,C++11 標準的草稿文件)
- 讓自己熟悉 Boost(準標準庫)
More Effective c++
- 仔細區別 pointers 和 references(當你知道你需要指向某個東西,而且絕不會改變指向其他東西,或是當你實現一個操作符而其語法需求無法由 pointers 達成,你就應該選擇 references;任何其他時候,請採用 pointers)
- 最好使用 C++ 轉型操作符(static_cast、const_cast、dynamic_cast、reinterpret_cast)
- 絕不要以多態(polymorphically)方式處理數組(多態(polymorphism)和指針算術不能混用;數組對象幾乎總是會涉及指針的算術運算,所以數組和多態不要混用)
- 非必要不提供 default constructor(避免對象中的字段被無意義地初始化)
- 對定製的 “類型轉換函數” 保持警覺(單自變量 constructors 可通過簡易法(explicit 關鍵字)或代理類(proxy classes)來避免編譯器誤用;隱式類型轉換操作符可改爲顯式的 member function 來避免非預期行爲)
- 區別 increment/decrement 操作符的前置(prefix)和後置(postfix)形式(前置式累加後取出,返回一個 reference;後置式取出後累加,返回一個 const 對象;處理用戶定製類型時,應該儘可能使用前置式 increment;後置式的實現應以其前置式兄弟爲基礎)
- 千萬不要重載 &&,|| 和 , 操作符(&& 與 || 的重載會用 “函數調用語義” 取代 “驟死式語義”;, 的重載導致不能保證左側表達式一定比右側表達式更早被評估)
- 瞭解各種不同意義的 new 和 delete(new operator、operator new、placement new、operator new[];delete operator、operator delete、destructor、operator delete[])
- 利用 destructors 避免泄漏資源(在 destructors 釋放資源可以避免異常時的資源泄漏)
- 在 constructors 內阻止資源泄漏(由於 C++ 只會析構已構造完成的對象,因此在構造函數可以使用 try…catch 或者 auto_ptr(以及與之相似的 classes) 處理異常時資源泄露問題)
- 禁止異常流出 destructors 之外(原因:一、避免 terminate 函數在 exception 傳播過程的棧展開(stack-unwinding)機制種被調用;二、協助確保 destructors 完成其應該完成的所有事情)
- 瞭解 “拋出一個 exception” 與 “傳遞一個參數” 或 “調用一個虛函數” 之間的差異(第一,exception objects 總是會被複制(by pointer 除外),如果以 by value 方式捕捉甚至被複制兩次,而傳遞給函數參數的對象則不一定得複製;第二,“被拋出成爲 exceptions” 的對象,其被允許的類型轉換動作比 “被傳遞到函數去” 的對象少;第三,catch 子句以其 “出現於源代碼的順序” 被編譯器檢驗對比,其中第一個匹配成功者便執行,而調用一個虛函數,被選中執行的是那個 “與對象類型最佳吻合” 的函數)
- 以 by reference 方式捕獲 exceptions(可避免對象刪除問題、exception objects 的切割問題,可保留捕捉標準 exceptions 的能力,可約束 exception object 需要複製的次數)
- 明智運用 exception specifications(exception specifications 對 “函數希望拋出什麼樣的 exceptions” 提供了卓越的說明;也有一些缺點,包括編譯器只對它們做局部性檢驗而很容易不經意地違反,與可能會妨礙更上層的 exception 處理函數處理未預期的 exceptions)
- 瞭解異常處理的成本(粗略估計,如果使用 try 語句塊,代碼大約整體膨脹 5%-10%,執行速度亦大約下降這個數;因此請將你對 try 語句塊和 exception specifications 的使用限制於非用不可的地點,並且在真正異常的情況下才拋出 exceptions)
- 謹記 80-20 法則(軟件的整體性能幾乎總是由其構成要素(代碼)的一小部分決定的,可使用程序分析器(program profiler)識別出消耗資源的代碼)
- 考慮使用 lazy evaluation(緩式評估)(可應用於:Reference Counting(引用計數)來避免非必要的對象複製、區分 operator[] 的讀和寫動作來做不同的事情、Lazy Fetching(緩式取出)來避免非必要的數據庫讀取動作、Lazy Expression Evaluation(表達式緩評估)來避免非必要的數值計算動作)
- 分期攤還預期的計算成本(當你必須支持某些運算而其結構幾乎總是被需要,或其結果常常被多次需要的時候,over-eager evaluation(超急評估)可以改善程序效率)