作為使用最為廣泛的語言之一,C++也出現在衆多開源項目中。為了有效管理C++豐富的文法所帶來的複雜性和提高可讀性,Google公司專門制定了C++代碼風格建議。為了便于自己養成良好的程式設計習慣,同時為國内同行提供便利,我将原文内容以及YuleFox等人的中文翻譯做一個精簡版的總結。
頭檔案
通常而言,每個.cc(.cpp)檔案應該對應一個.h頭檔案,但也有例外,例如包含main()函數的檔案。本小節主要讨論關于頭檔案的一些代碼風格規範。
Self-contained頭檔案
頭檔案應該能夠自給自足(也就是可以作為第一個頭檔案被引入),以.h結尾。至于用來插入文本的檔案,說到底它們并不是頭檔案,是以應該以.inc結尾。不允許分離出-inl.h頭檔案的做法。
所有頭檔案要能夠自給自足。換言之,使用者和重構工具不需要為特别場合而包含額外的頭檔案。詳言之,一個頭檔案要有define保護,統統包含它所需要的其它頭檔案(這意味着當使用者使用某一個.h頭檔案時,它不再需要包含該頭檔案所依賴的所有其它非庫頭檔案),也不要求定義任何特别的symbols。
例外:一個檔案并不是self-contained的,而是作為文本插入到代碼某處。或者檔案内容實際上是其它頭檔案的特定平台(platform-specific)擴充部分。這些檔案就需要用.inc檔案擴充名。
如果.h檔案聲明了一個模闆或者内聯函數,同時也在該檔案加以定義。凡是有用到這些的.cc檔案,就得統統包含該頭檔案,否則程式可能會在建構中連結失敗,不要把這些定義放到分裂的-inl.h檔案裡。
例外:如果某函數模闆為所有相關模闆參數顯式執行個體化,或者本身就是某類的一個私有成員,那麼它就隻能定義在執行個體化該模闆的.cc檔案裡。
#define保護
所有頭檔案都應該用#define來防止頭檔案被多重包含,命名格式應當是:<PROJECT>_<PATH>_<FILE>_H_.
為保證唯一性,頭檔案的命名應該基于所在項目源代碼樹的全路徑,例如項目foo中的頭檔案foo/src/bar/baz.h的#define保護方式應該為:FOO_BAR_BAZ_H_。
前置聲明
盡可能地避免使用前置聲明。使用#include包含需要的頭檔案即可。
定義:
- 所謂“前置聲明”(forward declaration)是類、函數和模闆的純粹聲明,沒有伴随着其定義。
優點:
- 前置聲明可以節省編譯時間,多餘的#include會迫使編譯器展開更多的檔案,處理更多的輸入。
- 前置聲明能夠節省不必要的重新編譯時間。#include使得代碼因為頭檔案中無關的改動而被重新編譯多次。
缺點:
- 前置聲明隐藏了依賴關系,頭檔案改動時,使用者的代碼會跳過必要的重新編譯過程。
- 前置聲明可能會被庫的後續更改所破壞。前置聲明函數或者模闆有時候會妨礙頭檔案開發者變動其API,例如擴大形參類型,加個自帶預設參數的模闆形參等等。
- 前置聲明來自命名空間std::的symbol時,其行為未定義。
- 很難判斷什麼時候該用前置聲明,什麼時候該用#include。極端情況下,用前置聲明代替#include甚至都會暗暗地改變代碼的含義:如果#include被B和D的前置聲明所代替,test()就會調用f(void*)。前置聲明了不少來自頭檔案的symbol時,就會比單單一行的#include冗長。*僅僅為了能前置聲明而重構代碼(比如用指針成員代替對象成員)會使代碼變得更慢更複雜。
結論:
- 盡量避免前置聲明那些定義在其它項目中的實體。
- 函數:總是使用#include。
- 類模闆:優先使用#include。
内聯函數
隻有當函數隻有10行甚至更少時才将其定義為内聯函數。
定義:
- 當函數被聲明為内聯函數之後,編譯器會将其内聯展開,而不是按通常的函數調用機制進行調用。
優點:
- 隻要内聯的函數體比較小,内聯該函數可以令目标代碼更加高效。對于存取函數以及氣他函數體比較短,性能關鍵的函數,鼓勵使用内聯。
缺點:
- 濫用内聯将導緻程式變得更慢。内聯可能使目标代碼量或增或減,這取決于内聯函數的大小。内聯函數非常短小的存取函數通常會減少代碼的大小,但内聯一個相當大的函數将戲劇性地增加代碼大小。現代處理器由于更好地李永樂指令緩存,小巧的代碼往往執行更快。
結論:
- 一個較為合理的經驗準則是:不要内聯超過10行的函數。謹慎對待析構函數,析構函數往往比其表面看起來要更長,因為有隐含的成員函數和基類析構函數被調用。
- 另一個實用的經驗準則:内聯那些包含循環或者switch語句的函數常常是得不償失的(除非在大多數情況下,這些循環或者switch語句從不被執行)。
- 有些函數即使聲明為内聯也不一定會被編譯器内聯:比如虛函數和遞歸函數就不會被正常内聯。通常遞歸函數不應該聲明成内聯函數。(遞歸調用堆棧的展開并不像循環那麼簡單,比如遞歸層數在編譯時可能是未知的,大多數編譯器都不支援内聯遞歸函數)。
#include的路徑及順序
使用标準的頭檔案包含順序可增強可讀性,避免隐藏依賴:相關頭檔案,C庫,C++庫,其他庫的.h,本項目内的.h。
項目内頭檔案應該按照源代碼目錄樹結構排列,避免使用UNIX特殊的快捷目錄.(目前目錄)或者..(上級目錄)。
例如dir/foo.cc的主要作用是實作或者測試dir2/foo2.h的功能,foo.cc中包含頭檔案的次序如下:
1. dir2/foo2.h(優先位置)
2. C系統檔案
3. C++系統檔案
4. 其他庫的.h檔案
5. 本項目内.h檔案
這種優順序保證當dir2/foo2.h遺漏某些必要的庫時,dir/foo.cc或者dir/foo_test.cc的建構會立刻終止。是以這一條規則保證維護這些檔案的人們首先看到建構終止的消息而不是維護其他包的人們。
dir/foo.cc和dir2/foo2.h通常位于同一目錄下,但也可以放在不同目錄下面。
按照字母順序對頭檔案包含進行二次排序是個不錯的注意。注意較老的代碼可能不符合這條規則,要在友善的時候修正它們。
您所依賴的symbols被哪些頭檔案所定義,您就應該包含哪些頭檔案,forward-declaration的情況除外。比如您要用bar.h中的某個symbol,哪怕您所包含的foo.h已經包含了bar.h,也照樣得包含bar.h,除非foo.h有明确說明它會自動向您提供bar.h中的symbol。不過凡是.cc檔案所對應的“相關頭檔案”已經包含的,就不用再重複包含進其.cc檔案裡面了,就像foo.cc隻包含foo.h就夠了,不用再管後者所包含的其它内容。
例外:有時平台特定(system-specific)代碼需要條件編譯(conditional include),這些代碼可以放到其它includes之後,當然平台特定代碼也要足夠簡練并且獨立,例如:
#include "foo/public/fooserver.h"
#inlucde "base/port.h" //For LANG_CXX11
#ifdef LANG_CXX11
#include <initializer_list>
#endif // LANG_CXX11
總結
- 避免多重包含是學程式設計的最基本要求。
- 前置聲明是為了降低編譯依賴,防止修改一個頭檔案引發多米諾效應。
- 内聯函數的合理使用可提高代碼執行效率。
- -inl.h可提高代碼的可讀性。
- 标準化函數參數順序可以提高可讀性和易維護性。
- 包含頭檔案的名稱使用比較完整的項目路徑看上去很清晰,有條理;包含的次序還可以減少隐藏依賴,使得每個頭檔案在“最需要編譯”的地方編譯。頭檔案都放在對應源檔案的最前面,這一點足以保證内部錯誤的及時發現了。
- 類内部的函數一般會自動内聯,是以某函數一旦不需要内聯,其定義就不要再放在頭檔案裡面了。
- 在#include中插入空行以分割相關頭檔案、C庫、C++庫,其它庫的.h以及本項目内的.h是個好習慣。