Google C++ Style Guide 繁體中文版

本書翻譯了 Google 官方訂定的 Coding Style (程式碼風格) 標準。

如果程式碼都能夠遵照同一份標準來撰寫的話,就能夠大大地增加程式的可讀性。 Google 所訂定的 C++ 程式碼風格非常明確,也非常仔細。 我認為是一個很不錯的標準。

不過可惜的是,Google 官方只有提供英文版的說明。為了讓不善英文,並使用中文的人能夠理解文件的內容,特別撰寫本書供大家閱讀。

連結

版本

目前此文的版本為:release-20210424-001

對應的原文版本

請注意原本的 Google C++ Style Guide 也會與時俱進,英文本的內容會不斷修改。 因此這裡標註出此書翻譯時對應的英文版原件:

需要支援

有些部分我個人覺得翻譯仍不太準確,而且翻得很爛。 例如「背景」那章的「風格指引的目標」下的內容,普遍來說翻譯應該都不太好。 因此我需要大家提供意見幫我改進那邊的翻譯。 基本上就對照中英文看一下,然後開一個 issue 讓我知道一下怎麼修改會比較好。

一些撰寫時的規則

  • 所有文句除標題外,一律以句號結尾。
  • 句號之後若還有其他語句在同一段,一律加上一個半形空白。
  • 英文單字與中文字詞的銜接處一律加上一個半形空白。

英文專有名詞的處理

這點我想了很久,有些程式中的關鍵字像是 class 或者 function 之類的,到底該使用中文的翻譯名詞呢?還是直接打英文名詞?

使用中文專有名詞的好處是,對於直接從中文教材學習的人來說,很快就可以進入狀況。 不過有可能我翻譯的名詞與這些人當初所學的不同,這樣或許也沒有好處。 而且對於本來就學英文的人來說,一下子看到中文名詞可能很難進入狀況。 事實上,我個人平常是中英文夾雜使用的。 遇到專有名詞就切換成英文,一般解釋時就用中文。 但是無論如何,我必須要訂出一個統一的標準貫穿本書,以方便讀者閱讀。

我想來想去,覺得這本書主要的目還是在於翻譯。因此除了程式碼的部分外,盡量少用英文名詞較好。 所以我最後的方針是:

除非是非得使用英文的情況,不然程式的專有名詞一律翻譯成中文。 少見名詞第一次出現時,在旁標明英文。 常用的名詞則直接使用中文翻譯。

為了方便只知道英文名詞的人能夠查閱看不懂翻譯的名詞,我在附錄列了一張中英專有名詞的對照表。 希望多少有點幫助。

回報與修正

如果覺得哪裡翻譯有誤,或者覺得語句不通順,歡迎到 Github repository 的 issue 提出問題。 如果熟悉 Git 的話,也歡迎在 Github 上 fork 這個 repository 自行修改,並開啟 pull request 請求合併。

背景

C++ 是許多 Google 開源專案 (Open Source Project) 中常用的程式語言。 如同許多 C++ 程式設計師所知,C++ 具有許多強大的特性,但是這也帶來許多複雜性,使得程式碼容易產生各種錯誤 (Bug),並且也難以閱讀及維護。

本指南的目標在於藉由說明那些東西該寫、哪些不該寫,來控制 C++ 程式碼的複雜度。 指南中提及的規則可以讓程式碼易於維護,同時也讓程式設計師能夠好好發揮 C++ 帶來的強大生產力。

Style (風格) 也稱作 Readability (可讀性),可以說是一種管理 C++ 程式碼的約定。 事實上,用 Style 這個字眼可能不太精確。 因為這類約定並非只有著墨在程式碼檔內的排版與格式。

幾乎所有 Google 開發的開源專案皆遵守著本指南。

注意:這份指引並非教你如何寫 C++,我們假設了讀者已經非常熟悉這個程式語言。

風格指引的目標

為什麼會有這份文件?

這邊有幾個核心目標是我們相信這份文件該提供的。 這些目標就是構成這些規定的主要原因。 藉由提出這些想法,我們希望能夠攤開來討論,並且讓大家理解為什麼會有這些規則,以及某些特別的規定是如何訂定出來的。 如果你能了解每個規則是為了甚麼目標而制定,那大家應該也能更容易了解某些規定被丟棄時,有哪些必要的考量以及反論。

這份文件目前我們認為的目標有以下幾點:

規則需要具有足夠的份量

一條風格規則所帶來的好處必須大到足夠合理來要求我們的工程師記住它。 它的好處是以沒有這份指南的情況下撰寫出的程式碼為準來衡量的。 有些規則可能在一些極端不好的程式碼中具有些微的好處,但是這種程式碼一般人不太可能會寫得出來的話,我們就不會寫進這份指引。 這個原則解釋了大多數我們沒有撰寫的規則,而不是我們會有寫出的規則。 舉例來說,goto 違反了下面很多條原則,但是基本上很少人會使用它,因此這份風格指引就不討論它。

並非為了撰寫者,而是為了讀者來優化

我們預計我們的程式碼庫 (以及大多數提交到上面的各個組件) 會持續維護很長的一段時間。 因此大多數的時間會花在讀懂它,而非撰寫它。 我們明確地選擇優化我們平凡工程師在於讀、維護以及偵錯程式碼的體驗,而非撰寫的便利性。 「為讀者留下足跡」正是一種在這個原則之下常見的觀點:如果在一段程式碼中正在發生一些驚喜或者不尋常的事情 (例如:轉移一個指標的所有權),在這裡留下一些文字上的提示將會非常有用。 (std::unique_ptr 明確地在呼叫處展示了所有權轉移)。

與現有的程式碼保持一致性

在我們程式庫中使用一致的風格讓我們可以專注在其他 (更重要) 的問題。 一致性同時也允許了自動化:那些自動幫你調整格式或者 #include 位置的工具只在你的程式碼如它預期般地一致時才能正常運作。 在很多狀況下,那些標記為「保持一致性」的規則其實是肇因於「不要想太多,挑一個就好」。 在那些情形允許彈性的潛在好處其實大於大家在那邊爭論所花費的時間。

適當時與盡可能大多數的 C++ 社群保持一致性

跟其他組織使用 C++ 的方式保持一致所帶來的價值與在我們自己的程式庫中保持一致性的理由是相同的。 如果一個 C++ 標準的特性可以解決一個問題,或者一個寫法被大眾所接受,那就可以當成使用它的理由。 然而,有時候這些特性與寫法可能具有某些缺陷,或者並不是我們的程式庫所需要的。 在這類情況下,其實更適合限制或者禁止這些特性。 在某些情形,如果我們無法感受到特別的好處,或者沒有足夠的價值,比起那些定義在 C++ 標準上的函式庫,我們更喜歡從頭到尾自己撰寫或者直接使用一個第三方的函式庫。

避免令人驚訝或者危險的結構

C++ 有著一些比看起來更令人訝異或者危險的特性。 這份風格指引內的部分限制就是為了要避免掉入這種陷阱裡。 風格指引對於放棄那些限制具有很高的標準,因為放棄那些規則很有可能會對程式的正確性造成很大的危機。

避免那些讓我們的平凡 C++ 程式設計師認為詭異或者難以維護的結構

有些 C++ 的特性可能一般來說不太適合被使用,因為它們可能常造成一些額外的複雜性。 在一些廣泛被使用的程式碼中可能會適合使用特別的語言結構,因為複雜實作所帶來的好處會被廣泛使用這點放大,而且了解這些複雜東西的代價不需要在寫新一塊程式碼時再度付出一次。 如果有疑問的話,可以向專案領導詢問是否可以拋棄這類的規則。 這對我們的程式庫特別重要,因為程式碼的管理團隊與團隊內部的成員會隨著時間改變:儘管現在正在使用某段程式碼的每個人都了解它,經過了幾年後也不能保證仍會是如此。

在我們的規模下要非常小心

對於一個超過 1 億行以及有著數千位工程師管理的程式庫來說,一個工程師的一些錯誤或者過於簡化的程式碼可能會造成嚴重的後果。 舉例來說,要特別注意不能汙染全域命名空間:如果大家都把東西放在全域命名空間的話,對於這種具有數億行程式碼的程式庫來說可能會很難避免命名衝突,並使得程式庫難以維護。

承認必要的優化

雖然有些效能上的優化可能會牴觸這份文件的一些原則,但是對於必要的情況來說還是可接受的。

結論

這份文件的用意在於最大化地提供指引與適當的限制。 一如往常,我們的盡量使規則符合常識或看起來順眼。 我們特別參考了整個 Google C++ 社群建立起的傳統,而非單獨考量你個人的喜好或者你的團隊。 當你看到不尋常的結構時,請善用你的智慧並保持懷疑:沒有限制它們並不代表你可以忽略它們。 此時請你自行判斷,如果你不太確定,請不要猶豫,立即詢問你的專案領導來取得意見。

C++ 版本

程式碼現在應該鎖定於 C++ 11 版。換句話說,不該使用 C++ 14 版與 C++ 17 版的功能。 這份指南鎖定的版本會 (積極地) 隨著時間跟進。

程式碼應該避免使用那些在最新版中 (目前是 C++ 17 版) 被移除的特性,以及那些少數在最新版中具有不同意義的特性。 某些 C++ 的特性是被限制或者不允許被使用的。 不要使用非標準的擴充套件 (Non-standard Extensions)

標頭檔 (Header Files)

一般來說,每一個 .cc 檔都應該要有一個對應的 .h 檔。 不過也有一些常見的例外,像是單元測試 (Unit Test) 跟一些只包含著 main() 的小型 .cc 檔就不需要有。

正確地使用標頭檔可以對可讀性、程式碼大小與效能帶來巨大的影響。

以下這些原則會引領你克服標頭檔中各式各樣的陷阱。

自給自足標頭檔 (Self-contained Headers)

標頭檔應該要自給自足 (self-contained),而且副檔名必須是 .h。 其他具有插入目的,但不是標頭檔者,則應該要使用 .inc 作為副檔名,並且應該盡少使用。

所有標頭檔都應該要自給自足。 換句話說,使用者或者重構工具 (Refractoring Tool) 並不需要依賴任何額外的條件才能夠插入標頭檔。 更精確地說,標頭檔應該要包含 標頭檔保護,而且應該要自己插入所有需要的其他標頭檔。

建議將模板與行內函式的定義放在同樣的檔案作為宣告。 所有使用到這些東西的 .cc 檔都應該要載入這些結構,不然在某些建置設定下會造成程式無法連結。 如果將定義與宣告分別在不同檔案,載入宣告的話也應該要能夠同時載入其定義。 不要將這些定義移至額外的 -inl.h 檔中。 這種作法在過去很常見,但是從現在開始不允許這麼做。

有一個例外是,函式模板的顯式實體化 (explicitly instantiated) 或者該模板是一個類別的私有成員的話,可以只定義在實體化該模板的 .cc 檔中。

在某些極少數的狀況下,標頭檔可以不用是自給自足的。 這些特殊的標頭檔通常是用來載入程式碼到做一些不尋常的位置,例如載入到另一個檔案的中間。 他們可以不使用 標頭檔保護,而且可能沒有載入他們所需的檔案。 這種類型的檔案應該使用 .inc 作為副檔名。 盡量別使用這種檔案,可能的話還是盡量用自給自足的標頭檔。

#define 保護 (The #define Guard)

所有的標頭檔應該要包含 #define 保護,以防止多重載入。 其名稱的格式為 <專案名稱>_<路徑>_<檔名>_H_

為了保證名稱的獨特性,應該要遵照該檔案在專案中的完整路徑來定義。 例如,一個在專案 foo 之中 foo/src/bar/baz.h 位置下的檔案,其保護應該要這樣寫:

#ifndef FOO_BAR_BAZ_H_
#define FOO_BAR_BAZ_H_

...

#endif  // FOO_BAR_BAZ_H_

前向宣告 (Forward Declarations)

盡可能地避免使用前向宣告。 只要 #include 你需要的標頭檔就好。

定義

類別、函式與模板的前向宣告指的就是在沒有定義其內容的情況下,預先宣告名稱的程式碼。

優點

  • 前向宣告可以節省編譯時間。 #include 會使得編譯器處理時必須要開啟更多檔案而且處理更多資料。
  • 前向宣告可以避免不必要的重新編譯。 #include 有可能在標頭檔做了些無關的修改時,使得其他相關的程式碼就得需要被重新編譯。

缺點

  • 前向宣告會隱藏某些依賴關係,可能會造成使用者的程式碼在標頭檔修改後略過了必要的重新編譯過程。

  • 一個前向宣告可能會因為其函式庫內部的修改而損壞。 函式與模板的前向宣告會造成撰寫標頭檔的人無法更改 API,像是增加函式的參數,給模板增加一個預設數值,或者轉移到一個新的名稱空間。

  • 前向宣告 std:: 名稱空間內的符號時常產生一些未定義的行為。

  • 有時候很難界定到底是否該在程式碼中使用前向宣告,或者全部使用 #include。 有時候替換掉 #include 可能會大幅改變程式碼的意義:

    // b.h:
    struct B {};
    struct D : B {};
    
    // good_user.cc:
    #include "b.h"
    void f(B*);
    void f(void*);
    void test(D* x) { f(x); } // calls f(B*)
    

    如果上面的程式碼中,將 #include 替換成 BD 的前向宣告的話,test() 就會呼叫 f(void*)

  • 從一個標頭檔前向宣告多個符號比單純地 #include 更難在發生錯誤時除錯。

  • 為了使用前向宣告而重構程式碼(像是把把物件成員換成指標),可能會造成程式變慢或者更加複雜。

我們的決定

  • 盡量避免前向宣告其他專案中的實體。
  • 當使用一個宣告在標頭檔內的函式時,總是 #include 那個標頭檔。
  • 當你要使用類別模板時,盡量使用 #include

請參考 #include 時的名稱與順序 來參考何時應該插入標頭檔

譯註

老實說我也是第一次看到「前向宣告」這個詞,因此我花了點時間研究這到底是什麼東西。 我稍微閱讀了維基百科上的資料之後,在此寫下我對這個這個詞的理解。

事實上,前向宣告這個詞簡單來說就是「在定義之前先宣告」。 相信應該不少人有宣告過函式吧?像是:

int sum(int, int);

這裏你可以看到我們並沒有寫說 sum 這個函式代表著什麼意思,只說他接受兩個整數參數,回傳一個整數值,而實際上 sum 的內容則定義在別的地方。 這個目的就是在告訴編譯器:「有個叫做 sum 的函式存在,等下使用時,請把 sum 這個符號當成一個接受兩個整數參數,回傳一個整數的函式處理。」

其實這就是一個「前向宣告」,也就是預先告知這個東西的存在,然後再另外定義內容。

前向宣告除了函式之外,也可以用在類別與模板上。 假設現在其他檔案中定義了 class A,而你的檔案需要用到它,你可以單純地宣告:

class A;

這樣就可以不用使用 #include 來載入相關的標頭檔。 但是這個使用有個限制,就是程式碼中使用到 class A 的地方,都只能使用指標或參考,而不能直接使用像是 A a 這樣的變數。 使用類別的前向宣告要注意的事情相對比較多,所以上面的建議才會提到大部份的情況還是直接 #include 比較好。

行內函式 (Inline Functions)

只有在函式程式碼少於或等於 10 行時才將它宣告為行內函式。

定義

透過宣告函式為行內函式,可以讓編譯器直接在呼叫該函式的地方展開函式,而不是遵照一般的函式呼叫機制編譯。

優點

只要函式夠小,將函式宣告為行內函式可以產生更有效率的目的碼 (object code)。 你可以盡量將存取函式 (accessor) 、修改函式 (mutator) 以及一些極短但對效能有巨大影響的函式行內化。

缺點

過度使用行內函式可能會造成程式變慢。 依照函式長度的不同,行內化可能會增加或減少程式碼的大小。 行內化一個很小的存取函式通常可以減少程式碼的長度,不過行內化一個很大的函式可能會巨幅地增加長度。 現在的處理器因為指令快取 (instruction cache) 的關係,處理較短的程式碼通常會更快。

我們的決定

一個適當的規則是不要將 10 行以上的函式行內化。 其中要特別注意解構函式 (destructors),解構函式常常比你所看到的還要長。 因為解構函式還會隱性地另外呼叫成員以及基底類別 (base class) 的解構函式。

另一個有用的規則: 一般來說將一個具有有迴圈或者 switch 的函式行內化對效能並沒有幫助 (除非大多數的情況下這個迴圈或 switch 都不會被執行到)。

要特別注意的是,就算將一個函式宣告為行內函式,編譯器也不一定會照做。 例如:虛擬函式 (virtual function) 或遞迴函式 (recursive function) 常常不會被行內化。 因為遞迴函式ㄧ般來說不該是行內函式。 至於將虛擬函式寫成行內函式的理由,通常只是為了要方便將函式的定義放在類別內而已 (例如類別的存取函式或修改函式)。

譯註

這邊提供一個行內函式的範例,inline 關鍵字是將函式行內化的關鍵:

inline int sum(int a, int b) {
	return a + b;
}

#include 時的名稱與順序

利用右列順序 #include 標頭檔,避免隱藏的依賴關係:直屬的標頭檔、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 檔內的東西,那麼 #include 的順序應該這樣寫:

  1. dir2/foo2.h
  2. C 系統檔
  3. C++ 系統檔
  4. 其他函式庫 .h
  5. 你的專案的 .h

依照這個順序,如果 dir2/foo2.h 遺漏了任何必要的 #include,那麼在建置 dir/foo.ccdir/foo_test.cc 的時候就會中斷。 因此這確保了建置會先在這些檔案中斷,而不是在其他無辜的地方發生。

dir/foo.cc 與其直屬的標頭檔 dir2/foo2.h 通常會放在同一個資料夾下,例如 base/basictypes_test.ccbase/basictypes.h,但是有時候也有可能會分開放。

注意那些為了與 C 相容的標頭檔,像是 stddef.h 通常都有對應的 C++ 版本 (例如 cstddef)。 雖然兩種版本都可行,但是記得要維持一致性。

每個區塊中的檔案應該要依照字母順序排列。 要注意一些比較老的專案中可能沒有遵照這個規則,這些錯誤都應該要等方便的時候修改過來。

你應該要 #include 所有包含你使用到任何符號的標頭檔 (除非你在一些不尋常的狀況中使用了前向宣告)。 如果你使用了 bar.h 中的符號,別期待你 #includefoo.h 之後,foo.h 裡面會包含著 bar.h,此時你應該也要 #include bar.h。 除非 foo.h 有明顯地展現出它提供了 bar.h 中的符號。 另外,已經在直屬的標頭檔中 #include 過的東西,可以不用在 cc 檔中 #include (像是 foo.cc 可以依賴在 foo.h 上)。

舉個範例,google-awesome-project/src/foo/internal/fooserver.cc 檔中的 #include 可能長這樣:

#include "foo/server/fooserver.h"

#include <sys/types.h>
#include <unistd.h>
#include <hash_map>
#include <vector>

#include "base/basictypes.h"
#include "base/commandlineflags.h"
#include "foo/server/bar.h"

特例

有時候,只能在某些系統中使用的程式碼需要有條件地 #include,這種就可以程式碼就可以放在所有 #include 之後。 當然,盡可能地讓這種程式碼越少且影響範圍越小越好。 例子:

#include "foo/public/fooserver.h"

#include "base/port.h"  // For LANG_CXX11.

#ifdef LANG_CXX11
#include <initializer_list>
#endif  // LANG_CXX11

作用域 (Scoping)

名稱空間 (Namespaces)

除了某些特殊情況外,程式碼應當放在名稱空間中。 名稱空間應該要有個跟專案名稱有關、獨一無二的名字,最好也把路徑考慮進去。 不要使用 using 指示詞 (using-directive),像是 using namespace foo。 不要使用行內名稱空間 (inline namespace)。 關於未被命名的名稱空間,請參考「無名名稱空間與靜態變數」一章。

定義

名稱空間將全域區分為多塊分開、各自命名的作用域,這可以有效避免在整個作用域中遇到名稱相同的狀況。

優點

名稱空間提供了一種避免在大型程式中名稱相衝的同時,也能讓大部分程式碼使用較短名稱的方法。

例如,兩個不同的專案在全域都具有 Foo 這個類別,這個符號可能會在編譯或執行時發生衝突。 如果這些專案能把他們的程式碼分別放在不同的名稱空間下,那麼 project1::Fooproject2::Foo 就會被視為不同的符號,也不會有衝突的問題,而且在各自的專案中還能夠繼續使用 foo 這個名字同時不需加上前綴。

行內名稱空間 (inline namespace) 會自動把它們的名稱放進作用域中。 例如,請參考以下程式碼:

namespace X {
inline namespace Y {
  void foo();
} // namespace Y
} // namespace X

這會產生 X::Y::foo() 等同於 X::foo() 的效果。 當初行內名稱空間的主要目標就是用來處理不同版本的 ABI (Application Binary Interface, 應用二進位介面) 的兼容問題。

缺點

名稱空間可能容易造成混淆,因為它在類別存在的情況下,又額外提供了一種切分命名作用域的方式。

行內名稱空間可能也會造成混淆,因為它的作用域有時並非如它當初宣告的那樣。 只有在某些需要分類不同版本的情況下才可能會有幫助。

在某些程式碼中,可能會需要使用完整的名稱來指出所需的名稱空間。 對於深層的名稱空間來說,這樣的寫法可能會造成一些混亂。

我們的決定

名稱空間應該如下列般使用:

  • 遵從 名稱空間命名 一章的規則。

  • 如同範例中在名稱空間的尾端加上註解。

  • 名稱空間必須將整個原始碼檔中,除了 #includegflags 定義和前向宣告其他名稱空間的類別之外的內容包裹起來。

    // 在某個 .h 檔中
    namespace mynamespace {
    
    // 所有的定義都在名稱空間的作用域內
    // 注意沒有使用縮排
    class MyClass {
    public:
      ...
      void Foo();
    };
    
    }  // namespace mynamespace
    
    // 在某個 .cc 檔中
    namespace mynamespace {
    
    // 函式的定義也放在名稱空間的作用域內
    void MyClass::Foo() {
      ...
    }
    
    }  // namespace mynamespace
    

    一般的的 .cc 檔中可能會有更多複雜的細節,例如 flags 或者 using 指示詞。

    #include "a.h"
    
    DEFINE_bool(someflag, false, "dummy flag");
    
    namespace mynamespace {
    
    using ::foo::bar;
    
    ...code for mynamespace...    // 程式碼左側不留空間
    
    }  // namespace mynamespace
    
  • 如果想將產生的協議訊息 (Protocol Message) 的程式碼放在命名空間中,請在 .proto 中使用 package 來指定。 詳情請參考 Protocol Buffer Packages

  • 別在 std 名稱空間下宣告任何東西,甚至是標準函式庫的前向宣告。 宣告 std 名稱空間下的作法沒有一個統一的標準,也就是說,這並不是一個跨平台的作法。 如果想要宣告標準函式庫內的實體,請直接 #include 那些標頭檔。

  • 你不該為了要讓某個名稱空間中的名稱皆可直接呼叫而使用「using 指示詞 (using-directive)」

    // 禁用 -- 這會汙染你的名稱空間
    using namespace foo;
    
  • 別在標頭檔中的名稱空間中使用名稱空間別名 (Namespace Alias),除非是在明確被標示為只給專案內部使用的名稱空間。 因為在一個被載入的標頭檔內的任何東西,都會被當作該檔的公共 API 對待。

    // 在 `.cc` 檔中簡化存取某個常用名稱的動作
    namespace fbz = ::foo::bar::baz;
    
    // 在 `.h` 檔中簡化存取某個常用名稱的動作
    namespace librarian {
    namespace impl {  // 內部區域,不是公共 API 的一部份
    namespace sidetable = ::pipeline_diagnostics::sidetable;
    }  // namespace impl
    
    inline void my_inline_function() {
      // 只在這個函式(或方法)內作用的命名空間別名
      namespace baz = ::foo::bar::baz;
      ...
    }
    }  // namespace librarian
    
  • 別使用行內名稱空間

譯註

就我所知,行內名稱空間是非常鮮見的東西,至少從我學習 C/C++ 到現在都沒有在一個實際的專案中看過。 這邊稍微介紹一下這個功能。

首先看下面這段範例:

namespace A {
namespace B {
void fun() {
    // code
}
}
}

一般來說,上面這段程式碼想要呼叫 fun 這個函式的話,就要使用 A::B::fun() 這個名稱。 但是如果此時引入了行內關鍵字,在 B 前面加上 inline,變成這樣:

namespace A {
inline namespace B {
void fun() {
    // code
}
}
}

這個時候你使用 A::fun() 這個名稱的話,就可以呼叫到 fun 這個函式。 當然,使用 A::B::fun() 也同樣可以呼叫得到 fun。 有興趣的話可以自己試試看。

運作的原理很簡單,其實就是程式在編譯時將所有 A::fun() 的名稱代換為 A::B::fun() 而已。

那這個技巧有甚麼用? 既然我想讓其他人直接呼叫到 fun,那為什麼還要特別放一個 namespace B

這個技巧最常用到的地方,就像上面 Google 文件中所提到的,版本控管。

如果今天我是在寫函式庫,此時我寫了兩種不同版本的 fun,可是有些人的程式已經連結到了舊版的 fun,那要怎麼樣在不重新編譯那些程式的狀況下保持連結? 這個時候可以這樣做:

假設原本的函式庫長這樣:

namespace SomeLib {
inline namespace v1 {
void fun() {
    // code
}
}
}

因此原本編譯的程式裡,呼叫到 SomeLib::fun 的地方都會連結到 SomeLib::v1::fun。 而此時我只要改成:

namespace SomeLib {
namespace v1 {
void fun() {
    // code
}
}
inline namespace v2 {
void fun() {
    // code
}
}
}

這樣原本連結到 SomeLib::v1::fun 的程式仍然可以運作,而且新的程式呼叫 SomeLib::fun 時,也可以如我預期地使用到新的版本 (v2 版)。 因此行內名稱空間才常用於函式庫的版本控管。 但是如同 Google Style 所說,這樣的行為可能實在會導致預期外的結果,所以還是盡量避免使用。

無名名稱空間與靜態變數

當你需要在 .cc 檔內放一些不會被其他檔案參考的的定義,請將這些東西放在一個無名名稱空間中,或者將他們全部宣告為 static。 不要在 .h 檔內使用任何這種結構。

定義

無名名稱空間可以對所有宣告給予內部連結性 (Internal Linkage)。 函數與變數則可以透過宣告為 static 來給予內部連結性。 這代表你宣告的東西不能被其他檔案內的東西給存取。 如果其他檔案中宣告了同樣名稱的東西,那麼這兩者會被視為獨立的個體。

我們的決定

我們鼓勵在 .cc 檔中對那些不需要被其他檔案參考的程式碼使用內部連結性,但不要在 .h 檔中使用。

無名名稱空間的格式與一般名稱空間相同。 結尾註解的部分,名稱空間的名稱留白即可:

namespace {
...
} // namespace

譯註

內部連結性 (Internal Linkage) 這個詞不常見,因此稍微解釋一下。 當今天有多個目的檔 (Object Files) 要合併成單一執行檔時,連結器 (Linker) 就會出動,並把這些檔案連結在一起。 具有內部連結性的變數或定義,具有可以在同一專案內被隨處使用,但是在專案外就無法使用的特性。 想要更深入了解可以參考這篇 說明文件(英文)

非成員、靜態成員、全域函式

盡量將非成員函式放在名稱空間中;少用完全的全域函式 (也就是說,不包含在任何名稱空間中)。 請不要把靜態函式包裝成一個類別。 類別的靜態成員一般來說應該要與類別的實例或者靜態資料有高度相關。

優點

非成員及靜態成員函式在某些狀況下很有用。 將非成員函式放在名稱空間中可以避免汙染全域的名稱空間。

缺點

非成員及靜態成員函式也許作為某個類別中的成員會更合理,特別是這些函式會存取一些外部資源或者有高度相關時。

我們的決定

有時候定義一個不受類別實例限制的函式很有用。 這樣的函式可以是非成員或者靜態成員函式。 非成員函式不應該依賴在某個外部變數上,而且應該放在某個名稱空間內。 不要特別為了一群函式建立一個類別,這與直接在函式前面加上前綴 (Prefix) 沒甚麼兩樣,而且這種包裝通常都不是必要的。

如果你定義了一個非成員函式,而且它只需要用在它所屬的 .cc 檔中,請使用 內部連結性 來限制它的作用域。

區域變數

將函式的變數盡可能地放在最小的作用域內,並在宣告變數的同時初始化

C++ 允許你在函式內的任何地方宣告變數。 我們鼓勵你盡可能地將變數宣告在越局部的 (local) 作用域越好,並且最好越靠近它第一次被使用的地方。 這讓讀者會更容易找到變數的宣告處以及了解它的型別是甚麼。 特別要注意初始化與宣告不該分開,例如:

int i;
i = f();      // 不好 -- 初始化與宣告分離
int j = g();  // 良好 -- 宣告同時初始化
vector<int> v;
v.push_back(1);  // 最好使用括號初始化法 (brace initialization)
v.push_back(2);
vector<int> v = {1, 2};  // 良好 -- v 一開始就初始化好

ifwhilefor 陳述句需要的變數一般來說應該要被宣告在那些陳述句中,這樣他們就會被限制在所屬的作用域內,例如:

while (const char* p = strchr(str, '/')) str = p + 1;

有一個例外:如果該變數是物件,那該物件的建構函式每次進入這個作用域時就會被呼叫一次,解構函式也會每次離開時都會被呼叫到。

// 低效率的寫法
for (int i = 0; i < 1000000; ++i) {
  Foo f;  // 我的建構函式與解構函式會各被呼叫 1000000 次
  f.DoSomething(i);
}

此時將這個變數宣告在迴圈外面,程式執行時會比較有效率:

Foo f;  // 我的建構函式與解構函式只會各被呼叫一次
for (int i = 0; i < 1000000; ++i) {
  f.DoSomething(i);
}

靜態與全域變數

不允許使用有著靜態生存期 (static storage duration) 的物件,除非他們是可平凡解構的 (trivially destructible)。 白話來說,這代表他的解構函式不做任何事情,包括他的成員與基底類別的解構函式在內。 更正式地說,這代表他的型別沒有使用者定義的或是虛擬解構函式 (virtual destructor),而且他的基底類別跟非靜態成員都是可平凡解構的。 函式內的區域變數可以使用動態初始化。 不鼓勵對靜態類別成員變數與位於命名空間作用域的變數使用動態初始化,但是在某些限定的情況下允許。 請看以下說明。

經驗法則:如果一個全域變數的宣告是 constexpr (常數表達式) 的話,他就符合這些要求。

定義

每個物件都有儲存期 (storage duration),這與他的存活期 (lifetime) 相關。 有著靜態儲存期 (static storage duration) 的物件從被初始化到程式結束都會存活。 這種物件出現在命名空間作用域作為一種全域變數、作為類別的靜態資料成員或是函式中帶有 static 字樣的區域變數。 函式區域靜態變數在執行經過他的宣告時被初始化;所有其他有著靜態儲存期的物件則作為程式啟動的一部分而同時被初始化。 所有有著靜態儲存期的物件在程式結束時被摧毀 (發生在所有尚未加入的執行緒終止之前)。

初始化可以是動態的,也就是說在初始化時可以發生一些不平凡 (non-trivial) 的事情。 (例如:一個會分配記憶體的建構函式,或是需要現在程序的 ID 才能初始化的變數。) 其他類的初始化都算是靜態初始化,雖然這兩者並不是完全相反的: 有著靜態儲存期的物件總是會先靜態初始化 (用一個給定的常數或者全部都是零的表示法初始化變數),然後需要的話才會動態初始化。

優點

全域與靜態變數對於大量的應用來說非常好用: 命名的常數、一些轉換元件中的輔助資料結構、命令提示的旗標 (flag)、日誌、註冊機制、背景基礎設施等等。

缺點

全域與靜態變數使用著動態初始化或者不平凡的解構函式產生的複雜度容易導致難以尋找的錯誤。 動態初始化在轉換元件之間或者解構時沒有被排序 (除了那種在反向初始化時的解構動作)。 當一個初始化指向另一個有著靜態儲存期的變數時,有可能會導致一個物件在他的存活期開始前 (或者結束之後) 被存取。 更甚者,當一個程式啟動了一些在結束時還沒加入的執行緒,這些執行緒可能會存取那些已經結束存活期的物件 (如果他的解構函式已經跑完的話)。

我們的決定

對於解構函式

當解構函式是平凡的 (trivial),他的執行完全並不會被順序所影響 (因為他們基本上不會執行);不然的話,我們會暴露在某些物件的存活期結束之後存取他們的風險之中。 因此我們只允許可平凡解構的物件擁有靜態儲存期。 基礎型別 (像是指標或者 int) 都是可平凡解構的,這些型別的陣列也都是。 注意被標示 constexpr 的變數也是可平凡解構的。

const int kNum = 10;  // 允許

struct X { int n; };
const X kX[] = {{1}, {2}, {3}};  // 允許

void foo() {
  static const char* const kMessages[] = {"hello", "world"};  // 允許
}

// 允許: constexpr 保證具備平凡的解構函式
constexpr std::array<int, 3> kArray = {{1, 2, 3}};
// 不好: 不是可平凡解構的
const string kFoo = "foo";

// 雖然 kBar 只是一個參考 (reference),但是因為相同原因所以不好。
// 注意這個規則也適用於延長存活期的暫存物件中。
const string& kBar = StrCat("a", "b", "c");

void bar() {
  // 不好: 非可平凡解構函式
  static std::map<int, int> kData = {{1, 0}, {2, 0}, {3, 0}};
}

注意參考並非物件,所以他們並不會被解構的限制所影響。 不過動態初始化的限制仍然適用。 特別是,一個具有 static T& t = *new T; 形式的函式內區域靜態變數是允許的。

對於初始化

初始化是一個更複雜的主題。 因為我們不只要考慮類別的建構式是否執行,也要考慮初始化時的計算:

int n = 5;    // 可以
int m = f();  // ? (根據 f 的行為決定)
Foo x;        // ? (根據 Foo::Foo 的行為決定)
Bar y = g();  // ? (根據 g 與 Bar::Bar 的行為決定)

除了第一式之外都帶來不確定的初始化順序問題。

我們這邊想要表達的概念在 C++ 標準的正式語言中稱為「常數初始化 (Constant Initialization)」。 這代表初始化的表示式 (expression) 是一個常數表示式,而且如果物件是使用建構函式初始化,那麼該建構函式也一定要標記 constexpr

struct Foo { constexpr Foo(int) {} };

int n = 5;  // 可以,5 是一個常數表示式
Foo x(2);   // 可以,2 是一個常數表示式,而且選擇的建構函式也是 constexpr
Foo a[] = { Foo(1), Foo(2), Foo(3) };  // 可以

我們允許使用常數初始化。 具有靜態儲存期的變數使用常數初始化的話,應該標上 constexpr 或是在可行的地方標上 ABSL_CONST_INIT 屬性。 任何沒有依照這種方式標記的、而且是不是區域變數的靜態儲存期變數,應該要盡可能地使用動態初始化,並且要仔細檢查。

作為反例,以下初始化是有問題的:

// 一些宣告
time_t time(time_t*);      // 沒有使用 constexpr!
int f();                   // 沒有使用 constexpr!
struct Bar { Bar() {} };

// 有問題的初始化
time_t m = time(nullptr);  // 初始化的表示是並非常數表示式
Foo y(f());                // 同上
Bar b;                     // 選用的建構函數 Bar::Bar() 也不是 constexpr

我們不鼓勵對於非區域變數使用動態初始化,而且一般來說也不允許。 然而,如果程式中沒有任何層面依賴該初始化與其他初始化之間的順序的話,我們就允許這麼做。 在這樣的限制下,初始化間的順序並不會造成顯著差異。 例如:

int p = getpid();  // 允許,只要沒有其他靜態變數使用 p 來初始化就好

允許對區域靜態變數使用動態初始化 (而且很常用)。

常見模式

  • 全域字串: 如果你要求使用全域或靜態字串常數,請考慮使用簡單的字元陣列或者一個指向字串字面值 (literal) 之中第一個字的字元指標。 字串字面值已經具有靜態儲存期,通常已足夠使用。
  • Map、Set、以及其他動態容器: 如果你要求使用靜態的、長度固定的資料集,像是一個 Set 或是一個查詢表,你不能把標準函式庫中的動態容器當成靜態變數使用,因為他們具有非平凡的解構函式。 反之,請考慮使用一個包含平凡型別的簡單陣列 (例如想要一個映射 int 到 int 的表,就建一個整數的陣列的陣列),或是一個 pair 的陣列 (例如包含 intconst char* 的 pair)。 對於小的資料集,線性搜尋就很夠了 (而且因為 memory locality 的關係,會很有效率)。 如果必要的話,保持資料集的順序性,並且使用二元搜尋法 (binary search)。 如果你真的比較想使用標準函式庫的動態容器,請考慮使用函式內的區域靜態指標,下面描述。
  • 智慧指標 (smart pointers): 智慧指標會在解構時執行清理動作,因此在上述狀況中被禁止使用。 請考慮你的使用狀況是否符合這章節中描述的其他模式。 一種簡單的解法是對動態分配的物件使用一般指標,然後永遠不要刪除它 (請看最後一項)。
  • 自製型別的靜態變數: 如果你要求對你自己設計的型別使用靜態的常數資料,請給他一個平凡的解構函式以及 constexpr 的建構函數。
  • 如果這些方法都不能用,你可以動態建立一個物件並將該物件綁訂在一個函式內的區域靜態指標變數,並且永遠都不要刪除它:static const auto* const impl = new T(args...); (如果初始化更複雜,你可以把它移入一個函式或者寫成一個 Lambda 運算式。)

thread_local 變數

對於不是在 function 內宣告的 thread_local 變數,一定要以編譯時期常數來初始化,而且一定要用 ABSL_CONST_INIT 屬性確保這件事。 偏好使用 thread_local 來定義執行緒內的區域變數。

定義

從 C++11 開始,變數可以在宣告時加上 thread_local

thread_local Foo foo = ...;

這樣的變數其實是對應到一堆物件的集合。 當不同的執行緒在存取變數的時候,其實會存取到不同的物件。 thread_local 變數其實在很多方面與 靜態儲存期變數 很接近。 舉例來說,它們可以被宣告在命名空間的作用域、函式內部、或是做為類別的靜態成員,但不能做為一般類別的成員。

thread_local 變數的實體的初始化很像靜態變數的作法,只差在它們必須在每個執行緒裡面分別初始化,而不是在程式啟動時一次初始化。 這代表著那些在函式內宣告的 thread_local 變數很安全,但是其他的 thread_local 變數則會像靜態變數一樣遭遇初始化順序的問題 (以及其他問題)。

thread_local 變數的實體在執行緒結束時被摧毀,所以他們沒有靜態變數的解構順序問題。

優點

  • 執行緒內資料從本質上來說不會受到 data races 影響 (因為只有一個執行緒可以存取),這點讓 thread_local 對並行程式設計很有幫助。
  • thread_local 是唯一定義在標準內,用來建立執行緒內變數 (thread-local variables) 的方式。

缺點

  • 存取 thread_local 的變數可能會觸發執行一些無法預期或是無法控制的程式碼。
  • thread_local 變數其實就是一個有效的全域變數,所以具備除了執行緒安全之外的所有全域變數的缺點。
  • thread_local 變數帶來的記憶體消耗會隨著執行緒數量增加而增長,有可能會造成程式中很大的負擔。
  • 一般類別的成員不能是 thread_local
  • thread_local 可能不比某些編譯器的內建功能 (compiler intrinsics) 還要有效率。

我們的決定

定義在函式內的 thread_local 變數沒有安全疑慮,因此可以被無限制的使用。 注意你可以利用定義一個會回傳 thread_local 變數的函式或靜態方法,來使用函式作用域等級的 thread_local 以模擬類別或是命名空間作用域等級的 thread_local

Foo& MyThreadLocalFoo() {
  thread_local Foo result = ComplicatedInitialization(); // 函式名稱:很複雜的初始化
  return result;
}

類別或是命名空間作用域等級的 thread_local 變數必須使用編譯期就決定的常數來初始化 (也就是說,不能具有任何動態初始化的部分)。 為了要強迫這個限制,類別內或是名稱空間內的 thread_local 變數一定要標註 ABSL_CONST_INIT (或是 constexpr,但不常用):

ABSL_CONST_INIT thread_local Foo foo = ...;

在定義執行緒內變數 (thread-local variables) 時應優先使用 thread_local

類別 (Classes)

類別是 C++ 程式碼中的基本元件。 我們平常會廣泛地使用它。 這章列出了你在撰寫類別的時候,應該以及不應該做的事情。

在建構函式內的工作

避免在建構函式內呼叫虛擬函式,而且避免在你無法送出錯誤訊號的時候進行一些可能失敗的初始化工作。

定義

建構函式內有可能會執行任意的初始化動作。

優點

  • 不需要擔心類別是否已經初始化了。
  • 已經經過建構函式充分初始化的物件可以作為 const 使用,而且與標準容器與演算法使用時更容易。

缺點

  • 如果在建構函式內呼叫虛擬函式,這些呼叫並不會如預期般地分配給衍伸類別內的實作。 就算你的類別現在還沒被繼承,未來針對你的類別的修改可能會默默地造成這種問題,並產生更多疑惑。
  • 沒有除了直接讓程式崩潰 (不是一直都是個好辦法) 或使用 Exception (被我們禁止了) 之外的簡單方法可以讓建構函式送出錯誤訊息。
  • 如果工作失敗了,那我們此時就會有個初始化失敗的物件。 這個時候我們可能會需要一個像是 bool IsValid() 的方法來檢查狀態,但是大家常會忘記要呼叫它。
  • 你無法取得建構函式的記憶體位址,所以建構函式中的工作無法簡單地被移交,例如交給另一個執行緒。

我們的決定

建構函式內部應該永遠不要呼叫虛擬函式。 如果你覺得合適的話,也許可以終止程式可以當作一種回報錯誤的方式。 除此之外,可以考慮像 TotW #42 所述那樣建立工廠方法 (Factory Method) 或者 Init() 方法。 避免在那些沒有儲存任何會影響到公用方法 (Public Methods) 的變數的物件中使用 Init() 方法 (有這種半建構式的物件存在時很難把程式寫對)。

隱性轉換 (Implicit Conversions)

不要定義隱性轉換。 在轉換運算子與接受單一引數的建構子上標註 explicit 關鍵字。

定義

隱性轉換允許將一種型別 (來源型別) 的物件用於原本預期另一種不同的型別 (目的型別) 的地方,像是將 int 的引數傳遞給接受 double 參數的函式。

除了語言本身定義的隱性轉換之外,使用者也可以藉由在來源型別或目的型別的類別中加入成員來自行定義。 在來源型別定義隱性轉換的操作可以藉由定義一個以目的型別命名的轉型運算子來達成,例如 operator bool() 可以讓該型別隱性轉換為 bool。 在目的型別定義隱性轉換的操作則可以藉由定義一個只接受來源型別為唯一參數的建構子達到 (或是唯一一個沒有預設值的參數)。

使用者可以在建構子或 (自從 C++11 之後) 在轉換運算子前加上 explicit 關鍵字,來確保只在使用當下明確知道目的型別 (例如強制轉型) 時才能使用。 這個限制除了應用於隱性轉換外,也會應用於 C++11 的 list 初始化語法:

class Foo {
  explicit Foo(int x, double y);
  ...
}

void Func(Foo f);
Func({42, 3.14}); // 錯誤

這類程式碼技術上來說並不算是隱性轉換,但是語言把它納入 explicit 關鍵字的規範中。

優點

  • 隱性轉換讓一個型別藉由在容易看出型別的場合消去明確寫出型別的必要,來使得該型別更易於使用且明瞭。
  • 隱性轉換可以當作比多載 (overloading) 更簡單的一種方案,例如可以免去一個名為 string_view 的函式為了 stringconst char* 兩種型別使用多載的必要。
  • List 初始化語法是一種用於初始化物件且簡潔明瞭的做法。

缺點

  • 隱性轉換會隱藏型別不合 (type-mismatch) 的 bug,像是目的型別不符合使用者的預期,或是使用者根本沒意識到有轉換發生。
  • 隱性轉換會讓程式碼更難以閱讀,特別是在跟多載同時出現的場合,這使得更難找出確切哪一段程式碼被呼叫到。
  • 單一引數的建構子可能會被意外地用於隱性型別轉換,就算這並非本意。
  • 如果一個單一引數的建構子沒有標註 explicit,並沒有一種可靠的方式能夠判斷到底是作者想要提供隱性轉換,還是只是單純忘記標註而已。
  • 總是難以搞清楚到底該讓來源型別或目的型別提供轉換,如果兩邊都提供了轉換方式,程式碼就變的曖昧不明。
  • List 初始化在目的型別不明確時也會遭遇相同問題,特別是在 list 中只有一個元素的時候。

我們的決定

型別轉換運算子與接受單一引數的建構子必須在類別的定義中標註 explicit。 一個例外是,複製與轉移建構子不應該是 explicit,因為他們並不會進行型別轉換。 隱性轉換在某些時候可以是必要的且對設計為透明包裝其他型別的型別來說是適當的。 對於這種情況請聯絡你的專案領導人來請求免去這個限制。

對於無法使用單一引數呼叫的建構子來說可以省略 explicit。 只接受單一 std::initializer_list 引數的建構子也應該省略 explicit,這樣才能支援複製初始化 (copy-initalization, 例如:MyType m = {1, 2}')。

建置中...

🚧 Struct 與 Class 的比較

建置中...

建置中...

建置中...

建置中...

建置中...

建置中...

🚧 其他 C++ 特性

🚧 預處理器巨集

命名 (Naming)

最重要的一致性規則是那些管理命名的規則。 一個名稱的風格可以讓我們在不需要搜尋一個實體的宣告的情況下,立即告知我們被命名的實體是哪種類型的東西:一個型別、一個變數、一個函數、一個常數、一個巨集等等。 我們大腦中的模式匹配 (Pattern-Matching) 引擎很高程度地仰賴這些命名規則。

命名的規則相當隨意,但是我們覺得在這個領域一致性比起個人的喜好更為重要,因此無論你發現這些規則是否明智,規則就是規則。

通用命名規則 (General Naming Rules)

命名應該要具有敘述的能力;避免使用縮寫。

讓一個名稱具有越清楚的描述越好,包括原因在內。 不要去想節省橫向的空間,因為讓你的程式碼能夠被新讀者馬上讀懂這事重要得多。 不要使用會讓專案以外的讀者看起來曖昧不明的縮寫,以及不要利用刪除一個單詞之間的字母來製造縮寫。 那些可能對專案外但是具有一些相關領域知識的人來說看得懂的縮寫還算可以接受。 經驗上來說,一個縮寫如果有被列在維基百科 (Wikipedia) 上的話,應該也可以接受。

符合規定:

int price_count_reader;    // 不含縮寫
int num_errors;            // 「num」是很廣泛運用的慣例
int num_dns_connections;   // 大部份的人都知道「DNS」代表甚麼
int lstm_size;             // 「LSTM」是機器學習 (Machine Learning) 領域中常見的縮寫

違反規定:

int n;                     // 沒意義
int nerr;                  // 曖昧的縮寫
int n_comp_conns;          // 曖昧的縮寫
int wgc_connections;       // 只有你的團隊才知道這代表甚麼
int pc_reader;             // 很多東西都可以縮寫成「pc」
int cstmr_id;              // 刪掉了一些中間字母
FooBarRequestInfo fbri;    // 這根本不是一個字

注意有些普遍知道的縮寫是可以接受的,像是 i 代表疊代 (iteration) 次數以及 T 代表模板 (template) 參數。

對於某些符號,這份指引建議對應的名稱以大寫字母作為開頭,並在每一個單詞的首字都使用大寫字母 (也就是所謂的駝峰式命名法)。 在這種名稱中如果出現縮寫或者首字型縮寫 (acronyms, 譯註:指的是用每個單字的第一個字母組成的縮寫) 的話,偏好將該縮寫或首字型縮寫視為一個單詞來處理 (像是 StartRpc(),而不是 StartRPC())。

模板參數應該要遵從所屬種類的命名風格:型別模板參數應該遵從型別名稱的規則,以及非型別模板參數應該要遵從變數名稱的規則。

檔案名稱

檔案名稱應該完全使用小寫,並可以包含底線 (_) 或是橫槓 (-)。 關於這點請遵循各專案的慣例。 如果沒有可以遵循且一致的本地模式,偏好使用 _

一些可接受的檔案名稱範例:

  • my_useful_class.cc
  • my-useful-class.cc
  • myusefulclass.cc
  • myusefulclass_test.cc // _unittest 與 _regtest 兩種寫法都過時了

C++ 檔案應該以 .cc 結尾,同時標頭檔應該以 .h 結尾。 依賴於在某個特定位置被文字引入的檔案應該以 .inc 結尾 (更多請看自給自足標頭檔的章節)。

不要使用已經存在於 /usr/include 的檔名,像是 db.h

一般來說,應該要讓你的檔案名稱非常精確。 例如,使用 http_server_logs.h 而不是 logs.h。 一個常見的狀況是使用相同名稱為一對檔案命名,像是 foo_bar.hfoo_bar.cc 定義了名為 FooBar 的類別。

型別名稱 (Type Names)

型別名稱始於一個大寫字母,並且每個單字的開頭皆為大寫字母,同時不包含底線:MyExcitingClassMyExcitingEnum

所有型別的名稱 - 類別、結構、型別別名、列舉、型別模板參數 - 都有相同的命名慣例。 型別名稱始於一個大寫字母,並且每個單字的開頭皆為大寫字母。 沒有底線。 例如:

// 類別與結構
class UrlTable { ...
class UrlTableTester { ...
struct UrlTableProperties { ...

// typedef
typedef hash_map<UrlTableProperties *, string> PropertiesMap;

// 使用別名
using PropertiesMap = hash_map<UrlTableProperties *, string>;

// 列舉
enum UrlTableErrors { ...

變數名稱 (Variable Names)

變數 (包含函式的參數) 與資料成員的名稱全部都使用小寫,並使用底線隔開單字。 類別 (但不包含結構) 的資料成員要另外在尾部加上底線。 例如,區域變數:a_local_variable、結構資料成員:a_struct_data_member、類別資料成員:a_class_data_member_

常見的變數名稱

可以接受的:

string table_name;  // 可以 - 使用底線
string tablename;   // 可以 - 全部小寫

不可接受的:

string tableName;   // 不好 - 大小寫混合

類別資料成員

類別資料成員,包括靜態 (static) 與非靜態的,都如同一般變數命名,但要在尾端加上底線。

class TableInfo {
  ...
 private:
  string table_name_;  // 可以 - 尾端有底線
  string tablename_;   // 可以
  static Pool<TableInfo>* pool_;  // 可以
};

結構資料成員

類別資料成員,包括靜態 (static) 與非靜態的,都如同一般變數命名。 他們不像類別一樣要在尾端加上底線。

struct UrlTableProperties {
  string name;
  int num_entries;
  static Pool<UrlTableProperties>* pool;
};

請看結構與類別章節的討論來瞭解何時該使用結構而不是類別。

常數名稱

使用 constexprconst 宣告的變數,以及在程式全期數值皆是固定的變數,其名稱應該以「k」為開頭,並使用大小寫混和的寫法。 底線可以用在少數無法使用大小寫明確分開的場合。例如:

const int kDaysInAWeek = 7;
const int kAndroid8_0_0 = 24;  // Android 8.0.0

所有具備以上性質同時有靜態儲存期 (例如靜態變數與全域變數,詳情請參照儲存期) 的變數都應該這樣命名。 這項慣例對其他儲存種類的變數來說是選擇性的 (例如自動變數),其他情況應使用一般的變數命名規則。

函數名稱

一般函式應使用大小寫混和;存取子 (Accessor) 與修改子 (Mutator) 可以使用變數的方式命名。

一般來說,函式應以大寫字母開頭,並在每一個新單字的首字使用大寫字母。

AddTableEntry()
DeleteUrl()
OpenFileOrDie()

(相同的命名規則也適用於類別與名稱空間範圍之中,那些作為 API 的一部分釋出的、或是意圖使人看起來像是函數的常數。 因為這些常數其實是物件而不是函式這點算是不重要的實作細節。)

存取子與修改子 (get 與 set 函式) 可以用變數的方式命名。 他們的命名通常與實際的成員變數有關,但這並非必要。 例如:int count()void set_count(int count)

名稱空間名稱

名稱空間全部使用小寫。 最頂層的名稱空間應使用基於專案名稱的名字。 避免巢狀名稱空間以及與知名頂層名稱空間的命名碰撞。

頂層的命名空間應通常使用專案名稱或者開發該程式的團隊名稱。 該名稱空間中的程式碼應該要放在與其名稱空間的命名相符的資料夾中 (或是子資料夾中)。

注意針對縮寫名稱的規則也如同變數一樣適用於名稱空間。 名稱空間中的程式碼很少會需要提及名稱空間的名稱,所以通常沒有使用縮寫的必要。

避免使用名稱與知名頂層名稱空間衝突的巢狀名稱空間。 名稱空間的命名衝突可能因為命名查詢規則造成意外的建置錯誤。 特別是不要建立任何命名為 std 的巢狀名稱空間。 優先使用獨特的專案 ID (websearch::indexwebsearch::index_util),而不是容易發生衝突的名稱,像是 websearch::util

對於 internal 名稱空間,請注意將其他程式碼加入同樣的 internal 名稱空間中會導致衝突 (團隊內部的輔助函式 (Helper Functions) 傾向於且可能導致衝突)。 在這種情況下,使用檔名來建立一個獨特的內部名稱很有用 (像是對 frobber.h 中的程式使用 websearch::index::frobber_internal)。

列舉器名稱

列舉器的值 (無論是否有限定範圍) 都應該要以常數或者巨集的方式命名:像是 kEnumNameENUM_NAME

可以的話,列舉器的值傾向於用常數的方式命名,不過使用巨集的方式也是可以接受的。 列舉器本身的名稱,例如 UrlTableErrors (與 AlternateUrlTableErrors),是一種型別,因此使用大小寫混和的命名方式。

enum UrlTableErrors {
  kOK = 0,
  kErrorOutOfMemory,
  kErrorMalformedInput,
};
enum AlternateUrlTableErrors {
  OK = 0,
  OUT_OF_MEMORY = 1,
  MALFORMED_INPUT = 2,
};

到 2009 年一月前,這個指引寫著應該要以巨集的方式命名。 這造成了列舉器的值與巨集的名稱衝突問題。 因此才改成了建議使用常數命名法。 新的程式碼應該在可以的時候皆使用常數命名法。 然而目前也沒有理由要把舊程式碼也改成常數命名法,除非舊程式碼造成了編譯時期的問題。

巨集名稱

你不是真要定義一個巨集吧? 如果真的要做的話,他們應該長這樣:MY_MACRO_THAT_SCARES_SMALL_CHILDREN_AND_ADULTS_ALIKE

請先閱讀關於巨集的說明; 一般來說不該使用巨集。 然而如果你真的需要使用,他們應該全部使用大寫與底線命名。

#define ROUND(x) ...
#define PI_ROUNDED 3.0

名稱規則的例外

如果你正在為類似於 C 或 C++ 內已經存在的實體進行命名,那麼你可以遵循他們慣例的命名規則。

  • bigopen()
    • 函式名稱,跟隨 open() 的形式命名
  • uint
    • typedef
  • bigpos
    • structclass,跟隨 pos 的形式命名
  • sparse_hash_map
    • 像是 STL 的實體;跟隨著 STL 的命名慣例
  • LONGLONG_MAX
    • 一個常數,像 INT_MAX 一樣命名

建置中...

建置中...

建置中...

建置中...

附錄:常用專有名詞中英對照表

下表以英文名詞按照字母順序排序

英文 (English)繁體中文 (Traditional Chinese)
Alias別名
Array陣列
Base Class基底類別
Bug錯誤
Class類別
Compile編譯
Compiler編譯器
Constructor建構函式
Declaration宣告
Define, Definition定義
Dependency依賴關係
Destructor解構函式
Directory資料夾、目錄
Entity實體
Enumerator列舉器
Expression表示式
External Linkage外部連結性
Forward Declaration前向宣告
Function函式
Global Scope全域
Handler(尚無翻譯)
Header, Header File標頭檔
Inline Function行內函式
Instance實例
Internal Linkage內部連結性
Input輸入值
Iteration疊代
Library函式庫
Link連結
Linker連結器
Literal字面值
Local局部的
Loop迴圈
Namespace名稱空間
Object Code目的碼
Object File目的碼檔
Output輸出值
Overloading多載
Parameter參數
Pointer指標
Prefix前綴
Private私有的
Private Member私有成員
Public公用的
Public Member公用成員
Raw Pointer原始指標
Reference參考
Smart Pointer智慧指標
Statement陳述句
Static Data Member靜態資料成員
Static Member Function靜態成員函式
Storage Duration儲存期
Struct結構
Symbol符號
Template模板
Thread執行緒
Type型別
Unnamed Namespaces無名名稱空間
Virtual Destructor虛擬解構函式
Virtual Function虛擬函式