Google C++ Style Guide私人解讀(1)

基於Revision 3.188

 

  頭文件

一般來說,每個.cc文件都應該有一個關聯的.h文件。有一些常見的例外,如單元測試代碼和只包含一個main()函數的.cc小文件,不在此列。

頭文件的正確使用能夠給您代碼的可讀性、尺寸和性能帶來極大的不同。

以下規則會讓您避開頭文件使用的誤區。

#define守衛

所有頭文件都要用#define守衛來避免多次包含。符號命名格式應爲<PROJECT>_<PATH>_<FILE>_H_

爲了保證唯一性,符號命名應該基於文件在項目源碼樹中的全路徑。比如說,foo項目中的文件foo/src/bar/baz.h應該放置以下的守衛:

#ifndef FOO_BAR_BAZ_H_

#define FOO_BAR_BAZ_H_

 

...

 

#endif  // FOO_BAR_BAZ_H_

個人觀點

Guard翻譯成守衛有點無厘頭,曾想翻譯成“後衛”,似乎更二,還是將就吧。這一條沒提到類庫的頭文件裏怎麼寫,看了一下glog/src/glog/log_severity.h,是BASE_LOG_SEVERITY_H__。有個問題:如果重構代碼時調整目錄結構的話,爲了遵守規範,所有頭文件都得改。

頭文件依賴關係

只要能用前遞聲明,就不用#include

每當在.cc文件裏包含一個頭文件,你就引入了一個依賴關係,只要這個頭文件有改動,你的.cc文件就得重新編譯。如果你的頭文件包含了別的頭文件,那麼這些“別的頭文件”只要有任何風吹草動,任何代碼只要包含了你的頭文件,就得重新編譯。因此,我們更希望把包含數量減少至最低限度,尤其是頭文件中包含的頭文件。

通過使用前遞聲明可以大量減少您自己的頭文件需要包含的頭文件數量。舉個例子,如果你的頭文件用到了File類,而你的用法並不需要涉及File類的聲明(此處的“聲明”疑爲“定義”之誤),那你就可以在頭文件裏使用前遞聲明“class File;”,而不必“#include "file/base/file.h"”。

那麼,在頭文件裏對Foo這個類的哪些用法不需要它的定義呢?

l        可以聲明Foo*Foo&型的數據成員。

l        可以聲明參數或返回值類型爲Foo的函數(但不能定義)(例外:如果參數類型爲Fooconst Foo&,而Foo又有個未聲明爲explicit的單參數構造函數,我們就需要Foo的完整定義,以支持自動類型轉換)

l        可以聲明Foo類型的靜態數據成員,因爲靜態數據成員是類外定義。

另一方面,如果繼承了Foo類,或是聲明瞭Foo型的數據成員,就必須包含Foo的頭文件了。

有的時侯,有理由用指針成員(或者更好的scoped_ptr)代替對象成員。然而,這讓代碼難懂,更收到效率罰單,所以如果只是爲了減少頭文件包含,那還是避免這種做法爲好。

當然,.如果cc文件用到某些類的話,一般都需要它們的定義,所以經常要包含好幾個頭文件。

注意:如果需要用到Foo這個符號,你應該自己引入它的定義,或者用#include,或者用前遞聲明。不要依賴間接包含進來的頭文件引入的符號。有個例外:如果myfile.cc用到了Foo,那在myfile.h而非myfile.cc#include(或前遞聲明)Foo是沒問題。

個人觀點

這條和Effective C++ 3rditem 31一致。編譯依賴也是個老話題了,小貝的《大規模C++程序設計》裏就提過。比較困惑的一點是動態鏈接庫,理論上講,如果對一個動態鏈接庫的頭文件有任何修改,則所有依賴其的程序都應該重新編譯一遍,纔不違反ODL,但實際上只要不影響動態庫裏類的行爲,不編譯也說得過去,而且不編譯才符合動態庫的要義。如果改變了動態庫裏C++類的大小、刪掉某個函數、增加/修改一個虛函數,就不能不重新編譯所有依賴這個動態庫的程序了,沒有完善的module機制真是悲劇,也不知有沒有朋友詳細瞭解過C++0xmodule提案,好不好使啊?

前遞聲明出來的類是incomplete class type,除了上面提到和類成員相關的幾處外,incomplete type還能用在:全局變量聲明(extern)、全局函數聲明中的參數和返回值、模板實參,不過都有很多限制,還是不要使用的好。另外,聲明一個返回值爲incomplete type的虛函數時也有限制:在重寫虛函數並使用covariant返回值的時候,返回值的類型必須有完整定義(否則誰知道它是不是子類呢)

需要用到Foo時要求自己將其#include這條要求不錯,一是看代碼時從#include塊一眼就能看出用到些什麼,二是免得不小心用到不該用的東西,不過二暫時還沒想到例子。

內聯函數

函數很小,比如說10行以下時,才定義成內聯函數。

定義:你能夠將函數聲明爲允許編譯器內聯展開對它們的調用,而不是通過通常的函數調用機制來調用。

優點:只要內聯函數很小,編譯器將其內聯展開就能生成更高效的代碼。你可以放心地將set/get函數以及其它較短的性能關鍵函數聲明爲內聯。

缺點:過度使用內聯會讓程序在實際中跑得更慢。根據函數的長短,將其內聯可能導致生成的代碼尺寸變大或變小。內聯一個很小的get/set函數通常會減小代碼尺寸,而內聯一個很大的函數會使代碼尺寸大大增加。在現代處理器上,通常尺寸更小的代碼運行得更快,因爲指令緩存將得到更好的利用。

決定:近期的經驗總結是不要內聯一個10行以上的函數。對待析構函數要小心,它們經常比乍看起來更長,因其隱含了對成員和基類析構函數的調用。

另一個有用的經驗總結是:對具有循環或switch語句的函數進行內聯在典型情況下效果不大(除非循環或switch語句一般不會執行)

有件重要的事情必須知道:即使聲明成內聯,函數也不是總會內聯;例如,虛函數和遞歸函數正常情況下都不會內聯。遞歸函數通常不應內聯,而將虛函數設爲內聯主要是爲了將其定義放在類定義裏,無論是出於方便考慮還是爲了文檔化其行爲,如set/get函數。

個人觀點

這條基本和Effective C++ 3rdItem 30一樣。

英語爲啥支持這麼長的句子呢,一堆which,看懂一句話既要look ahead,又要look back,誰有心得?

-inl.h文件

有必要時,可以用文件名後綴爲-inl.h的文件放置複雜內聯函數的定義。

內聯函數的定義必須放在頭文件裏,這樣編譯器才能將此定義在調用處展開。然而,實現代碼正確的位置還是在.cc文件裏,我們不希望在.h文件中有太多的實際代碼,除非這樣做能提升可讀性或性能。

如果一個內聯函數的定義很短,幾乎不包含什麼邏輯,那就應該放在頭文件裏。例如,set/get函數一定要放在類定義中。如能方便函數的實現者和調用者,更復雜的內聯函數也可以放到.h文件中——如果這樣讓頭文件變得笨重,你也可以把函數代碼放到一個獨立的-inl.h文件裏。這樣把實現從類定義中分離了出來,而在必要時仍能夠把實現包含進來。

-inl.h文件的另外一個用法是用來定義函數模板。這種方法能用來保持你的模板定義容易讀懂。

不要忘了,一個.inl.h文件和別的頭文件一樣,需要一個#define守衛。

個人觀點

除了能把template的定義和實現分開放,實在想不到這麼做的理由……不過據說Linux裏有這麼搞的,一個.c文件到處包含。

函數形參的順序

定義一個函數時,形參順序是:先輸入,後輸出。

C/C++的形參或是函數的輸入,或是函數的輸出,或者既是輸入又是輸出。入參通常是值類型或const引用,而出參及輸入/輸出形參是非const指針。當安排函數形參的順序時,將所有隻做入參的形參放置在任何出參之前。特別地,爲函數新增形參時,不要僅僅因爲是新增的,就把它們放到參數列表末尾;新的只入形參仍要放到所有出參前面。

這並非一個硬性規定,既做輸入又做輸出的形參(常爲類/結構)使情況變得複雜一些,而且,爲了和相關的函數保持一致,可能需要放寬規則,就像你總是做的那樣。

個人觀點

先入後出順序和出參類型的規定總感覺不太舒服,比方爲std::string定義一個replace函數:

std::string& replace(

std::string& s,

const std::string& from,

const std::string& to

);

s放在最前並聲明爲引用更符合我的口味,好在本條並非硬性規定(希望翻譯沒錯)

名字和包含順序

爲了可讀性,也爲了避免隱藏的依賴關係,使用標準順序:C標準庫,C++標準庫,其它庫的.h,你項目的.h

所有的項目頭文件在列出時都應該使用從項目源碼目錄開始的相對路徑,不使用UNIX目錄的快捷方式“.(當前目錄)和“..(上級目錄)。例如:google-awesome-project/src/base/logging.h在包含時應該寫作:

#include "base/logging.h"

dir/foo.ccdir/foo_test.cc——主要用來實現和測試dir2/foo2.h中的內容——中,按以下方式安排你的頭文件包含順序:

1.      dir2/foo2.h(推薦放在此處——請看下面詳述)

2.      C的系統文件。

3.      C++系統文件。

4.      其它庫的.h文件。

5.      你項目的.h文件。

這種推薦的排序減少了隱藏的依賴關係。我們想讓每個頭文件能單獨編譯通過。達到這一目標最容易的方法是保證每個頭文件都是某個.cc文件中第一個#include.h文件。

dir/foo.ccdir2/foo2.h一般放在同一個目錄裏(base/basictypes_test.ccbase/basictypes.h),但也可以在不同目錄裏。

在每段包含語句中都按字母序排序則更加宜人。

例如,google-awesome-project/src/foo/internal/fooserver.cc中的包含語句可能是這樣的:

#include "foo/public/fooserver.h"  // 推薦放在此處。

#include <sys/types.h>

#include <unistd.h>

#include <hash_map>

#include <vector>

#include "base/basictypes.h"

#include "base/commandlineflags.h"

#include "foo/public/bar.h"

個人觀點

本條的內容比較考究,爲了避免間接使用某個符號(“頭文件依賴關係”一節中提到),每個頭文件都是某個.cc文件裏#include段的排頭兵,這樣頭文件就不用在註釋裏寫上“得先包含那個,再包含這個”了,值得參考。

至於各類頭文件的包含順序,google自己也未能遵守,如glog/src/logging.cc中的包含順序就和上面描述的不同,還是按個人習慣來吧。

#include時要求寫明頭文件完整路徑很好,如果遵守這個條例,可以避免一些詭異的問題。比如我自己遇到過的一個問題:

項目用到一個開源類庫IM,需要升級到新版本。舊版IM的頭文件在/usr/include下,庫文件在/usr/lib下,新版IM則安裝在/usr/local/include/IM/usr/local/lib下。

由於安裝程序建了個子目錄IM存放頭文件,我們就把代碼從#include <IM.h>改成#include <IM/IM.h>後,編譯器默認先搜索/usr/local/,再搜索/usr/,按理說應該沒有問題。但我們在rebuild時鏈接不過,大大的見鬼。

查了半天,終於查到是IM的頭文件IM.h寫得有問題,這個天殺的IM.h的內容如下:

#include <IMCore.h> // 注意是尖括號

#include <IMXXX.h> // 同上

編譯過程如下,編譯器看到尖括號,就跳過當前目錄,直接搜索系統頭文件路徑,不在/usr/local/include裏?那就去/usr/include下把舊愛找回來吧,於是IMCore.hIMXXX.h都是在/usr/include裏的舊版本。但鏈接器卻找到了正確版本的庫,於是出錯。真是庫不如新,頭不如故。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章