視C++為一個語言聯邦
C++高效編程守則視狀況而變化,取決於你使用C++的哪一部分。
盡量以const,enums,inline替換#define
對於單純常量,最好以const對象或enums替換#defines;
對於形似函數的宏(macros),最好改用inline函數替換#defines。
盡可能使用const
將某些東西聲明為const可幫助編譯器偵測出錯誤用法。const可被施加於任何作用域內的對象、函數參數、函數返回類型、成員函數本體;
編譯器強制實施bitwise constness,當你編寫程序時應該使用“概念上的常量性”(conceptual constness)
當const和non-const成員函數有著實質等價的實現時,令non-const版本調用const版本可避免代碼重復。
確定對象被使用前已先被初始化
為內置性對象進行手工初始化,因為C++不保證初始化它們;
構造函數最好使用成員初值列(member initialization list),而不要在構造函數本體內使用賦值操作(assignment)。初值列列出的成員變量,其排列次序應該和它們在class中的聲明次序相同;
為免除“跨編譯單元值初始化次序”問題,輕易local static對象替換non-local static對象。
了解C++默默編寫並調用哪些函數
編譯器可以暗自為class創建default構造函數、copy構造函數、copy assignment操作符,以及析構函數。
若不想使用編譯器自動生成的函數,就該明確拒絕
為駁回編譯器自動(暗自)提供的機能,可將相應的成員函數聲明為private並且不予以實現。
為多態基類聲明virtual析構函數
polymorphic(帶多態性質的)base classes應該聲明一個virtual析構函數。如果class帶有任何virtual函數,它就應該擁有一個virtual析構函數;
Classes的設計目的如果不是作為base classes使用,或不是為了具備多態性(polymorphically),就不該聲明virtual析構函數。
別讓異常逃離析構函數
析構函數絕對不要吐出異常。如果一個被析構函數調用的函數可能拋出異常,析構函數應該捕獲任何異常,然後吐下它們(不傳播)或結束程序;
如果客戶需要對某個操作函數運行期間拋出的異常做出反應,那麼class應該提供一個普通函數(而非在析構函數中)執行該操作。
絕不在構造和析構過程中調用virtual函數
在構造和析構期間不要調用virtual函數,因為這類調用從不下降至derived class(比起當前執行構造函數和析構函數的那層)。
令operator=返回一個reference to *this
令賦值(assignment)操作符返回一個reference to *this。
在operator=中處理“自我賦值”
確保當對象自我賦值時operator=有良好行為。其中技術包括比較“來源圖像”和“目標對象”的地址、精心周到的語句順序、以及copy-and-swap;
確定任何函數如果操作一個以上的對象,而其中多個對象時同一個對象時,其行為仍然正確。
復制對象時勿忘其每一個成分
Copying函數應該確保復制“對象內的所有成員變量”及“所有base class成分”;
不要嘗試以某個copying函數實現另一個copying函數。應該講共同機能放進第三個函數中,並由兩個copying函數共同調用。
以對象管理資源
為防止資源洩露,請使用RAII對象,它們在構造函數中獲得資源並在析構函數中釋放資源;
兩個常常被使用的RAII classes分別是tr1::shared_ptr和auto_ptr。前者通常是較佳選擇,因為其copy行為比較直觀。若選擇auto_ptr,復制動作會使它(被復制物)指向null。
在資源管理類中小心copying行為
復制RAII對象必須一並復制它所管理的資源,所以資源的copying行為決定RAII對象的copying行為;
普遍而常見的RAII class copying行為是:抑制copying、施行引入計數法(reference counting)。不過其他行為也都可能被實現。
在資源管理類中提供對原始資源的訪問
APIs往往要求訪問原始資源(raw resources),所以每一個RAII class應該提供一個“取得其所管理之資源”的辦法;
對原始資源的訪問可能經由顯示轉換或隱式轉換。一般而言顯示轉換比較安全,但隱式轉換對客戶比較方便。
成對使用new和delete時要采取相同的形式
如果你在new表達式中使用[],必須在相應的delete表達式中也是用[]。如果你在new表達式中不使用[],一定不要在相應的delete表達式中使用[]。
以獨立語句將newed對象置入智能指針
以獨立語句將newed對象存儲於(置入)智能指針內。如果不這樣做,一旦異常被拋出,有可能導致難以察覺的資源洩露。
讓接口容易被正確使用,不容易被誤用
好的接口很容易被正確使用,不容易被誤用。你應該在你的所有接口中努力達成這些性質;
“阻止誤用”的辦法是建立新類型、限制類型上的操作,束縛對象值,以及消除客戶的資源管理任務;
tr1::shared_ptr支持定制型刪除器(custom deleter)。這可防范DLL問題,可被用來自動解除互斥鎖(mutexes)等等。
設計class猶如設計type
Class的設計就是type的設計。在定義一個新type之前,請確定考慮新type對象的創建和銷毀、對象的初始化和賦值、合法值、繼承關系等。
寧以pass-by-reference-to-const替換pass-by-value
盡量以pass-by-reference-to-const替換pass-by-value。前者通常比較高效,並可避免切割問題;
以上規則並不適用於內置類型,以及STL的迭代器和函數對象。對它們而言,pass-by-value往往比較適當。
必須返回對象時,別妄想返回其reference
絕不要返回pointer或reference指向一個local stack對象,或返回reference指向一個heap-allocated對象,或返回pointer或reference指向一個local static對象而有可能同時需要多個這樣的對象。
將成員變量聲明為private
切記將成員變量聲明為private。這可賦予客戶訪問數據的一致性、可細微劃分訪問控制、允許約束條件獲得保證,並提供class作者以充分的實現彈性;
protected並不比public更具封裝性。
寧以non-member、non-friend替換member函數
寧可拿non-member non-friend函數替換member函數。這樣做可以增加封裝性、包裹彈性(packaging flexibility)和機能擴充性。
若所有參數皆需類型轉換,請以此采用non-member函數
如果你需要為某個函數的所有參數(包括被this指針所指的那個隱喻參數)進行類型轉換,那麼這個函數必須是個non-member。
考慮寫出一個不拋出異常的swap函數
當std::swap對你的類型效率不高時,提供一個swap成員函數,並確定這個函數不拋出異常;
如果你提供一個member swap,也該提供一個non-member swap用來調用前者。對於classes(而非templates),也請特化std::swap;
調用swap時應針對std::swap使用using聲明式,然後調用swap並且不帶任何“命名空間資格修飾”;
為“用戶定義類型”進行std templates全特化是好的,但千萬不要嘗試在std內加入某些對std而言全新的東西。
盡可能延後變量定義式的出現時間
盡可能延後變量定義式的出現。這樣做可增加程序的清晰度並改善程序效率。
盡量少做轉型動作
如果可以,盡量避免轉型,特別是在注重效率的代碼中避免dynamic_casts。如果有個設計需要轉型動作,試著發展無需轉型的替代設計;
如果轉型是必要的,試著將它隱藏於某個函數背後。客戶隨後可以調用該函數,而不需要將轉型放進他們自己的代碼內;
寧可使用C++ style(新式)轉型,不要使用舊式轉型。前者很容易辨識出來,而且也比較有著分門別類的職掌。
避免返回handles指向對象內部成分
避免返回handles(包括references、指針、迭代器)指向對象內部。遵守這個條款可增加封裝性,幫助const成員函數的行為像個const,並將發生“虛吊號碼牌”的可能性降至最低。
為“異常安全”而努力是值得的
異常安全函數(Exception-safe functions)即使發生異常也不會洩露資源或允許任何數據結構敗壞。這樣的函數區分為三種可能的保證:基本型、強烈型、不拋異常型;
“強烈保證”往往能夠以copy-and-swap實現出來,但“強烈保證”並非對所有函數都可實現或具備現實意義;
函數提供的“異常安全保證”通常最高只等於其所調用之各個函數的“異常安全保證”中的最弱者。
透徹了解inlining的裡裡外外
將大多數inlining限制在小型、被頻繁調用的函數身上。這可使日後的調試過程和二進制升級(binary upgradability)更容易,也可使潛在的代碼膨脹問題最小化,使程序的速度提升機會最大化;
不要只因為function templates出現在頭文件,就將它們聲明為inline。
將文件間的編譯依存關系降至最低
支持“編譯依存性最小化”的一般構想是:相依於聲明式,不要相依於定義式。基於此構想的兩個手段是Handle classes和Interface classes;
程序庫頭文件應該以“完全且僅有聲明式”(full and declaration-only forms)的形式存在。這種做法不論是否涉及templates都適用。
確定你的public繼承塑模出is-a關系
“public繼承”意味is-a。適用於base classes身上的每一件事情一定也適用於derived classes身上,因為每一個derived class對象也都是一個base class對象。
避免遮蔽繼承而來的名稱
derived classes內的名稱會遮掩base classes內的名稱。在public繼承下從來沒有人希望如此;
為了讓被遮掩的名稱再見天日,可使用using聲明式或轉交函數(forwarding functions)。
區分接口繼承和實現繼承
接口繼承和實現繼承不同。在public繼承之下,derived classes總是繼承base class的接口;
pure virtual函數只具體指定接口繼承;
簡樸的(非純)impure virtual函數具體指定接口繼承及缺省實現繼承;
non-virtual函數具體指定接口繼承以及強制性實現繼承。
考慮virtual函數以外的其他選擇
virtual函數的替代方案包括NVI手法及Strategy設計模式的多種形式。NVI手法自身是一個特殊形式的Template Method設計模式;
將機能從成員函數移到class外部函數,帶來的一個缺點是,非成員函數無法訪問class的non-public成員;
tr1::function對象的行為就像一般函數指針。這樣的對象可接納“與給定之目標簽名式(target signature)兼容”的所有可調用物(callable entities)。
絕不重定義繼承而來的non-virtual函數
絕不要重新定義繼承而來的non-virtual函數。
絕不重新定義繼承而來的缺省參數值
絕對不要重新定義一個繼承而來的缺省參數值,因為缺省參數值都是靜態綁定,而virtual函數——你唯一應該覆寫的東西——卻是動態綁定。
通過復合塑模出has-a或“根據某物出現”
復合(composition)的意義和public繼承完全不同;
在應用域(application domain),復合意味has-a(有一個)。在實現域(implementation domain),復合意味著is-implemented-in-terms-of(根據某物實現出)。
明智而審慎地使用private繼承
Private繼承意味著is-implement-in-terms-of(根據某物實現出)。它通常比復合(composition)的級別低。但是當derived class需要訪問protected base class的成員,或需要重新定義繼承而來的virtual函數時,這麼設計是合理的;
和復合(composition)不同,private繼承可以造成empty base最優化。這對致力於“對象尺寸最小化”的程序開發者而言,可能很重要。
明智而審慎地使用多重繼承
多重繼承比單一繼承復雜。它可能導致新的歧義性,以及對virtual繼承的需要;
virtual繼承會增加大小、速度、初始化(及賦值)復雜度等等成本。如果virtual base classes不帶任何數據,將是最具實用價值的情況;
多重繼承的確有正當用途。其中一個情節涉及“public繼承某個Interface class”和“private繼承某個協助實現的class”的兩相結合。
了解隱式接口和編譯器多態
classes和template都支持接口(interfaces)和多態(polymorphism);
對classes而言接口是顯示的(explicit),以函數簽名為中心。多態則是通過virtual函數發生於運行期;
對templates參數而言,接口是隱式的(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生成多個classes和多個函數,所以任何template代碼都不該與某個造成膨脹的template參數產生相依關系;
因非類型模板參數(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)後,traits classes有可能在編譯期對類型執行if…else測試。
認識template元編程
Template metaprogramming(TMP,模板元編程)可將工作由運行期移往編譯期,因而得以實現早期錯誤偵測和更高的執行效率;
TMP可被用來生成“基於政策選擇組合”(based on combinations of pilicy choices)的客戶定制代碼,也可用來避免生成對某些特殊類型並不適合的代碼。
了解new-handler的行為
set_new_handler允許客戶指定一個函數,在內存分配無法獲得滿足時被調用;
Nothrow new是一個頗為局限的工具,因為它只適用於內存分配;後繼的構造函數調用還是可能拋出異常。
了解new和delete的合理替換時機
有許多理由需要寫個自定的new和delete,包括改善效能、對heap運用錯誤進行調試、收集heap使用信息。
編寫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在內的標准程序庫
C++標准程序庫的主要機能由STL、iostreams、locales組成。並包含C99標准程序庫;
TR1添加了只能指針(如tr1::shared_ptr)、一般化函數指針(tr1::function)、hash-based容器、正則表達式(regular expression)以及另外10個組件的支持;
TR1自身只是一份規范。為獲得TR1提供的好處,需要一份實物。一個好的實物來源是Boost。
讓自己熟悉Boost
Boost是一個社群,也是一個網站。致力於免費、源碼開放、同僚復審的C++程序庫開發。Boost在C++標准化過程中扮演具有影響力的角色;
Boost提供許多TR1組件實現品,以及其他許多程序庫。