5. 其他 C++ 特性

5.1. 引用參數

Tip

所有按引用傳遞的參數必須加上 const.

定義:

在 C 語言中, 如果函式需要修改變數的值, 參數必須為指標, 如 int foo(int *pval). 在 C++ 中, 函式還可以宣告引用參數: int foo(int &val).

優點:

定義引用參數防止出現 (*pval)++ 這樣醜陋的程式碼. 像拷貝建構子這樣的應用也是必需的. 而且更明確, 不接受 NULL 指標.

缺點:

容易引起誤解, 因為引用在語法上是值變數卻擁有指標的語義.

結論:

函式參數列表中, 所有引用參數都必須是 const:

void Foo(const string &in, string *out);

事實上這在 Google Code 是一個硬性約定: 輸入參數是值參或 const 引用, 輸出參數為指標. 輸入參數可以是 const 指針, 但決不能是非 const 的引用參數,除非用於交換,比如 swap().

有時候,在輸入形參中用 const T* 指標比 const T& 更明智。比如:

  • 你會傳 null 指標。
  • 函式要把指標或對地址的引用賦值給輸入形參。

總之大多時候輸入形參往往是 const T&. 若用 const T* 說明輸入另有處理。所以若你要用 const T*, 則應有理有據,否則會害得讀者誤解。

5.2. 右值參考

Tip

只在定義移動建構子與移動賦值操作時使用右值參考. 不要使用 std::forward.

定義:

右值參考是一種只能綁定到臨時對象的引用的一種, 其語法與傳統的引用語法相似. 例如, void f(string&& s); 宣告了一個其參數是一個字符串的右值參考的函式.

優點:

用於定義移動建構子 (使用類的右值參考進行建構的函式) 使得移動一個值而非拷貝之成為可能. 例如, 如果 v1 是一個 vector<string>, 則 auto v2(std::move(v1)) 將很可能不再進行大量的數據複製而只是簡單地進行指標操作, 在某些情況下這將帶來大幅度的性能提升.

右值參考使得編寫通用的函式封裝來轉發其參數到另外一個函式成為可能, 無論其參數是否是臨時對象都能正常工作.

右值參考能實作可移動但不可拷貝的類型, 這一特性對那些在拷貝方面沒有實際需求, 但有時又需要將它們作為函式參數傳遞或塞入容器的類型很有用.

要高效率地使用某些標準函式庫類型, 例如 std::unique_ptr, std::move 是必需的.

缺點:

右值參考是一個相對比較新的特性 (由 C++11 引入), 它尚未被廣泛理解. 類似引用崩潰, 移動建構子的自動推導這樣的規則都是很複雜的.

結論:

只在定義移動建構子與移動賦值操作時使用右值參考, 不要使用 std::forward 功能函式. 你可能會使用 std::move 來表示將值從一個對象移動而不是複製到另一個對像.

5.3. 函式重載

Tip

若要用好函式重載,最好能讓讀者一看呼叫點(call site)就胸有成竹,不用花心思猜測呼叫的重載函式到底是哪一種。該規則適用於建構子。

定義:

你可以編寫一個參數類型為 const string& 的函式, 然後用另一個參數類型為 const char* 的函式重載它:

class MyClass {
    public:
    void Analyze(const string &text);
    void Analyze(const char *text, size_t textlen);
};

優點:

通過重載參數不同的同名函式, 令程式碼更加直觀. 樣板化程式碼需要重載, 同時為使用者帶來便利.

缺點:

如果函式單單靠不同的參數類型而重載(acgtyrant 註:這意味著參數數量不變),讀者就得十分熟悉 C++ 五花八門的匹配規則,以瞭解匹配過程具體到底如何。另外,當派生類只重載了某個函式的部分變體,繼承語義容易令人困惑。

結論:

如果你打算重載一個函式, 可以試試改在函式名裡加上參數信息。例如,用 AppendString()AppendInt() 等, 而不是一口氣重載多個 Append().

5.4. 預設參數

Tip

我們不允許使用缺省函式參數,少數極端情況除外。盡可能改用函式重載。

優點:

當你有相依性預設參數的函式時,你也許偶爾會修改修改這些缺省參數。通過缺省參數,不用再為個別情況而特意定義一大堆函式了。與函式重載相比,缺省參數語法更為清晰,程式碼少,也很好地區分了「必選參數」和「可選參數」。

缺點:

預設參數會干擾函式指標,害得後者的函式簽名(function signature)往往對不上所實際要呼叫的函式簽名。即在一個現有函式添加缺省參數,就會改變它的類型,那麼呼叫其地址的程式碼可能會出錯,不過函式重載就沒這問題了。此外,缺省參數會造成臃腫的程式碼,畢竟它們在每一個呼叫點(call site)都有重複(acgtyrant 註:我猜可能是因為呼叫函式的程式碼表面上看來省去了不少參數,但編譯器在編譯時還是會在每一個呼叫程式碼裡統統補上所有默認實參信息,造成大量的重複)。函式重載正好相反,畢竟它們所謂的「缺省參數」只會出現在函式定義裡。

結論:

由於缺點並不是很嚴重,有些人依舊偏愛預設參數勝於函式重載。所以除了以下情況,我們要求必須顯式提供所有參數(acgtyrant 註:即不能再通過缺省參數來省略參數了)。

其一,位於 .cc 文件裡的靜態函式或匿名空間函式,畢竟都只能在局部文件裡呼叫該函式了。

其二,可以在建構子里用預設參數,畢竟不可能取得它們的地址。

其三,可以用來模擬可變長度陣列。

// 通過空 AlphaNum 以支持四個形參
string StrCat(const AlphaNum &a,
              const AlphaNum &b = gEmptyAlphaNum,
              const AlphaNum &c = gEmptyAlphaNum,
              const AlphaNum &d = gEmptyAlphaNum);

5.5. 可變長度陣列和 alloca()

Tip

我們不允許使用可變長度陣列和 alloca().

優點:

可變長度陣列具有渾然天成的語法. 變長數組和 alloca() 也都很高效.

缺點:

可變長度陣列和 alloca() 不是標準 C++ 的組成部分. 更重要的是, 它們根據數據大小動態分配堆棧內存, 會引起難以發現的內存越界 bugs: “在我的機器上運行的好好的, 發佈後卻莫名其妙的掛掉了”.

結論:

改用更安全的分配器(allocator),就像 std::vectorstd::unique_ptr<T[]>.

5.6. 友元

Tip

我們允許合理的使用友元類及友元函式.

通常友元應該定義在同一文件內, 避免程式碼讀者跑到其它文件查找使用該私有成員的類. 經常用到友元的一個地方是將 FooBuilder 宣告為 Foo 的友元, 以便 FooBuilder 正確建構 Foo 的內部狀態, 而無需將該狀態暴露出來. 某些情況下, 將一個單元測試類宣告成待測類的友元會很方便.

友元擴大了 (但沒有打破) 類的封裝邊界. 某些情況下, 相對於將類成員宣告為 public, 使用友元是更好的選擇, 尤其是如果你只允許另一個類訪問該類的私有成員時. 當然, 大多數類都只應該通過其提供的公有成員進行互操作.

5.7. 例外

Tip

我們不使用 C++ 例外.

優點:

  • 例外允許應用高層決定如何處理在底層嵌套函式中「不可能發生」的失敗(failures),不用管那些含糊且容易出錯的錯誤程式碼(acgtyrant 註:error code, 我猜是C語言函式返回的非零 int 值)。
  • 很多現代語言都用例外。引入異常使得 C++ 與 Python, Java 以及其它類 C++ 的語言更一脈相承。
  • 有些第三方 C++ 庫相依性例外,禁用異常就不好用了。
  • 例外是處理建構子失敗的唯一途徑。雖然可以用工廠函式(acgtyrant 註:factory function, 出自 C++ 的一種設計模式,即「簡單工廠模式」)或 Init() 方法代替異常, but these require heap allocation or a new “invalid” state, respectively.
  • 在測試框架裡很好用。

缺點:

  • 在現有函式中添加 throw 語句時,你必須檢查所有呼叫點。要麼讓所有呼叫點統統具備最低限度的例外安全保證,要麼眼睜睜地看異常一路歡快地往上跑,最終中斷掉整個程式。舉例,f() 呼叫 g(), g() 又呼叫 h(), 且 h 丟出的異常被 f 捕獲。當心 g, 否則會沒妥善清理好。
  • 還有更常見的,例外會徹底擾亂程式的執行串流程並難以判斷,函式也許會在你意料不到的地方返回。你或許會加一大堆何時何處處理異常的規定來降低風險,然而開發者的記憶負擔更重了。
  • 例外安全需要RAII和不同的編碼實踐. 要輕鬆編寫出正確的異常安全程式碼需要大量的支持機制. 更進一步地說, 為了避免讀者理解整個呼叫表, 異常安全必須隔絕從持續狀態寫到 “提交” 狀態的邏輯. 這一點有利有弊 (因為你也許不得不為了隔離提交而混淆程式碼). 如果允許使用異常, 我們就不得不時刻關注這樣的弊端, 即使有時它們並不值得.
  • 啟用例外會增加二進制文件數據,延長編譯時間(或許影響小),還可能加大地址空間的壓力。
  • 濫用例外會變相鼓勵開發者去捕捉不合時宜,或本來就已經沒法恢復的「偽異常」。比如,使用者的輸入不符合格式要求時,也用不著拋異常。如此之類的偽異常列都列不完。

結論:

從表面上看來,使用例外利大於弊, 尤其是在新專案中. 但是對於現有程式碼, 引入異常會牽連到所有相關程式碼. 如果新專案允許異常向外擴散, 在跟以前未使用異常的程式碼整合時也將是個麻煩. 因為 Google 現有的大多數 C++ 程式碼都沒有異常處理, 引入帶有異常處理的新程式碼相當困難.

鑒於 Google 現有程式碼不接受例外, 在現有程式碼中使用異常比在新專案中使用的代價多少要大一些. 遷移過程比較慢, 也容易出錯. 我們不相信異常的使用有效替代方案, 如錯誤程式碼, 斷言等會造成嚴重負擔.

我們並不是基於哲學或道德層面反對使用例外, 而是在實踐的基礎上. 我們希望在 Google 使用我們自己的開源專案, 但專案中使用異常會為此帶來不便, 因此我們也建議不要在 Google 的開源專案中使用異常. 如果我們需要把這些專案推倒重來顯然不太現實.

對於 Windows 程式碼來說, 有個 特例.

(YuleFox 注: 對於例外處理, 顯然不是短短幾句話能夠說清楚的, 以建構子為例, 很多 C++ 書籍上都提到當建構失敗時只有異常可以處理, Google 禁止使用異常這一點, 僅僅是為了自身的方便, 說大了, 無非是基於軟件管理成本上, 實際使用中還是自己決定)

5.8. 運行時類型識別

TODO

Tip

我們禁止使用 RTTI.

定義:

RTTI 允許開發者在運行時識別 C++ 類對象的類型. 它通過使用 typeid 或者 dynamic_cast 完成.

優點:

RTTI 的標準替代 (下面將描述) 需要對有問題的類層級進行修改或重構. 有時這樣的修改並不是我們所想要的, 甚至是不可取的, 尤其是在一個已經廣泛使用的或者成熟的程式碼中.

RTTI 在某些單元測試中非常有用. 比如進行工廠類測試時, 用來驗證一個新建對象是否為期望的動態類型. RTTI 對於管理對象和派生對象的關係也很有用.

在考慮多個抽像對象時 RTTI 也很好用. 例如:

bool Base::Equal(Base* other) = 0;
bool Derived::Equal(Base* other) {
  Derived* that = dynamic_cast<Derived*>(other);
  if (that == NULL)
    return false;
  ...
}

缺點:

在運行時判斷類型通常意味著設計問題. 如果你需要在運行期間確定一個對象的類型, 這通常說明你需要考慮重新設計你的類.

隨意地使用 RTTI 會使你的程式碼難以維護. 它使得基於類型的判斷樹或者 switch 語句散佈在程式碼各處. 如果以後要進行修改, 你就必須檢查它們.

結論:

RTTI 有合理的用途但是容易被濫用, 因此在使用時請務必注意. 在單元測試中可以使用 RTTI, 但是在其他程式碼中請盡量避免. 尤其是在新程式碼中, 使用 RTTI 前務必三思. 如果你的程式碼需要根據不同的對象類型執行不同的行為的話, 請考慮用以下的兩種替代方案之一查詢類型:

虛函式可以根據子類類型的不同而執行不同程式碼. 這是把工作交給了對象本身去處理.

如果這一工作需要在對象之外完成, 可以考慮使用雙重分發的方案, 例如使用訪問者設計模式. 這就能夠在對像之外進行類型判斷.

如果程式能夠保證給定的父類別實例實際上都是某個派生類的實例, 那麼就可以自由使用 dynamic_cast. 在這種情況下, 使用 dynamic_cast 也是一種替代方案.

基於類型的判斷樹是一個很強的暗示, 它說明你的程式碼已經偏離正軌了. 不要像下面這樣:

if (typeid(*data) == typeid(D1)) {
  ...
} else if (typeid(*data) == typeid(D2)) {
  ...
} else if (typeid(*data) == typeid(D3)) {
...

一旦在類層級中加入新的子類, 像這樣的程式碼往往會崩潰. 而且, 一旦某個子類的屬性改變了, 你很難找到並修改所有受影響的程式碼塊.

不要去手工實作一個類似 RTTI 的方案. 反對 RTTI 的理由同樣適用於這些方案, 比如帶類型標籤的類繼承體系. 而且, 這些方案會掩蓋你的真實意圖.

5.9. 類型轉換

Tip

使用 C++ 的類型轉換, 如 static_cast<>(). 不要使用 int y = (int)xint y = int(x) 等轉換方式;

定義:

C++ 採用了有別於 C 的類型轉換機制, 對轉換操作進行歸類.

優點:

C 語言的類型轉換問題在於模稜兩可的操作; 有時是在做強制轉換 (如 (int)3.5), 有時是在做類型轉換 (如 (int)"hello"). 另外, C++ 的類型轉換在查找時更醒目.

缺點:

噁心的語法.

結論:

不要使用 C 風格類型轉換. 而應該使用 C++ 風格.

  • static_cast 替代 C 風格的值轉換, 或某個類指標需要明確的向上轉換為父類別指針時.
  • const_cast 去掉 const 限定符.
  • reinterpret_cast 指標類型和整數或其它指針之間進行不安全的相互轉換. 僅在你對所做一切瞭然於心時使用.

至於 dynamic_cast 參見 5.8. 運行時類型識別.

5.10. 串流

Tip

只在記錄日誌時使用串流.

定義:

串流用來替代 printf()scanf().

優點:

有了串流, 在打印時不需要關心對象的類型. 不用擔心格式化字符串與參數列表不匹配 (雖然在 gcc 中使用 printf 也不存在這個問題). 流的建構和解構子會自動打開和關閉對應的文件.

缺點:

串流使得 pread() 等功能函式很難執行. 如果不使用 printf 風格的格式化字符串, 某些格式化操作 (尤其是常用的格式字符串 %.*s) 用流處理性能是很低的. 流不支持字符串操作符重新排序 (%1s), 而這一點對於軟件國際化很有用.

結論:

不要使用串流, 除非是日誌介面需要. 使用 printf 之類的代替.

使用串流還有很多利弊, 但程式碼一致性勝過一切. 不要在程式碼中使用流.

拓展討論:

對這一條規則存在一些爭論, 這兒給出點深層次原因. 回想一下唯一性原則 (Only One Way): 我們希望在任何時候都只使用一種確定的 I/O 類型, 使程式碼在所有 I/O 處都保持一致. 因此, 我們不希望使用者來決定是使用串流還是 printf + read/write. 相反, 我們應該決定到底用哪一種方式. 把日誌作為特例是因為日誌是一個非常獨特的應用, 還有一些是歷史原因.

串流的支持者們主張流是不二之選, 但觀點並不是那麼清晰有力. 他們指出的流的每個優勢也都是其劣勢. 流最大的優勢是在輸出時不需要關心打印對象的類型. 這是一個亮點. 同時, 也是一個不足: 你很容易用錯類型, 而編譯器不會報警. 使用流時容易造成的這類錯誤:

cout << this;   // 輸出地址
cout << *this;  // 輸出值

由於 << 被重載, 編譯器不會報錯. 就因為這一點我們反對使用操作符重載.

有人說 printf 的格式化醜陋不堪, 易讀性差, 但串流也好不到哪兒去. 看看下面兩段程式碼吧, 實作相同的功能, 哪個更清晰?

cerr << "Error connecting to '" << foo->bar()->hostname.first
     << ":" << foo->bar()->hostname.second << ": " << strerror(errno);

fprintf(stderr, "Error connecting to '%s:%u: %s",
        foo->bar()->hostname.first, foo->bar()->hostname.second,
        strerror(errno));

你可能會說, “把串流封裝一下就會比較好了”, 這兒可以, 其他地方呢? 而且不要忘了, 我們的目標是使語言更緊湊, 而不是添加一些別人需要學習的新裝備.

每一種方式都是各有利弊, “沒有最好, 只有更適合”. 簡單性原則告誡我們必須從中選擇其一, 最後大多數決定採用 printf + read/write.

5.11. 前置自增和自減

Tip

對於迭代器和其他樣板對象使用前綴形式 (++i) 的自增, 自減運算子.

定義:

對於變數在自增 (++ii++) 或自減 (--ii--) 後表達式的值又沒有沒用到的情況下, 需要確定到底是使用前置還是後置的自增 (自減).

優點:

不考慮返回值的話, 前置自增 (++i) 通常要比後置自增 (i++) 效率更高. 因為後置自增 (或自減) 需要對表達式的值 i 進行一次拷貝. 如果 i 是迭代器或其他非數值類型, 拷貝的代價是比較大的. 既然兩種自增方式實作的功能一樣, 為什麼不總是使用前置自增呢?

缺點:

在 C 開發中, 當表達式的值未被使用時, 傳統的做法是使用後置自增, 特別是在 for 循環中. 有些人覺得後置自增更加易懂, 因為這很像自然語言, 主語 (i) 在謂語動詞 (++) 前.

結論:

對簡單數值 (非對象), 兩種都無所謂. 對迭代器和樣板類型, 使用前置自增 (自減).

5.12. const 用法

Tip

我們強烈建議你在任何可能的情況下都要使用 const. 此外有時改用 C++11 推出的 constexpr 更好。

定義:

在宣告的變數或參數前加上關鍵字 const 用於指明變量值不可被篡改 (如 const int foo ). 為類中的函式加上 const 限定符表明該函式不會修改類成員變量的狀態 (如 class Foo { int Bar(char c) const; };).

優點:

大家更容易理解如何使用變數. 編譯器可以更好地進行類型檢測, 相應地, 也能生成更好的程式碼. 人們對編寫正確的程式碼更加自信, 因為他們知道所呼叫的函式被限定了能或不能修改變量值. 即使是在無鎖的多線程程式撰寫中, 人們也知道什麼樣的函式是安全的.

缺點:

const 是入侵性的: 如果你向一個函式傳入 const 變數, 函式原型宣告中也必須對應 const 參數 (否則變量需要 const_cast 類型轉換), 在呼叫庫函式時顯得尤其麻煩.

結論:

const 變數, 數據成員, 函式和參數為編譯時類型檢測增加了一層保障; 便於盡早發現錯誤. 因此, 我們強烈建議在任何可能的情況下使用 const:

  • 如果函式不會修改傳你入的引用或指標類型參數, 該參數應宣告為 const.
  • 盡可能將函式宣告為 const. 訪問函式應該總是 const. 其他不會修改任何數據成員, 未呼叫非 const 函式, 不會返回數據成員非 const 指標或引用的函式也應該宣告成 const.
  • 如果數據成員在對象建構之後不再發生變化, 可將其定義為 const.

然而, 也不要發了瘋似的使用 const. 像 const int * const * const x; 就有些過了, 雖然它非常精確的描述了常數 x. 關注真正有幫助意義的信息: 前面的例子寫成 const int** x 就夠了.

關鍵字 mutable 可以使用, 但是在多線程中是不安全的, 使用時首先要考慮線程安全.

const 的位置:

有人喜歡 int const *foo 形式, 不喜歡 const int* foo, 他們認為前者更一致因此可讀性也更好: 遵循了 const 總位於其描述的對象之後的原則. 但是一致性原則不適用於此, “不要過度使用” 的宣告可以取消大部分你原本想保持的一致性. 將 const 放在前面才更易讀, 因為在自然語言中形容詞 (const) 是在名詞 (int) 之前.

這是說, 我們提倡但不強制 const 在前. 但要保持程式碼的一致性! (Yang.Y 注: 也就是不要在一些地方把 const 寫在類型前面, 在其他地方又寫在後面, 確定一種寫法, 然後保持一致.)

5.13. constexpr 用法

Tip

在 C++11 裡,用 constexpr 來定義真正的常數,或實作常量初始化。

定義:

變數可以被宣告成 constexpr 以表示它是真正意義上的常數,即在編譯時和運行時都不變。函式或建構子也可以被宣告成 constexpr, 以用來定義 constexpr 變量。

優點:

如今 constexpr 就可以定義浮點式的真・常數,不用再相依性字面值了;也可以定義使用者自定義類型上的常量;甚至也可以定義函式呼叫所返回的常量。

缺點:

若過早把變數優化成 constexpr 變量,將來又要把它改為常規變量時,挺麻煩的;Current restrictions on what is allowed in constexpr functions and constructors may invite obscure workarounds in these definitions.

結論:

靠 constexpr 特性,方才實作了 C++ 在介面上打造真正常數機制的可能。好好用 constexpr 來定義真・常量以及支持常量的函式。Avoid complexifying function definitions to enable their use with constexpr. 千萬別癡心妄想地想靠 constexpr 來強制程式碼「內聯」。

5.14. 整數

Tip

C++ 內建整數中, 僅使用 int. 如果程式中需要不同大小的變數, 可以使用 <stdint.h> 中長度精確的整數, 如 int16_t.如果你的變量可能不小於 2^31 (2GiB), 就用 64 位變量比如 int64_t. 此外要留意,哪怕你的值並不會超出 int 所能夠表示的範圍,在計算過程中也可能會溢出。所以拿不準時,乾脆用更大的類型。

定義:

C++ 沒有指定整數的大小. 通常人們假定 short 是 16 位, int 是 32 位, long 是 32 位, long long 是 64 位.

優點:

保持宣告統一.

缺點:

C++ 中整數大小因編譯器和體系結構的不同而不同.

結論:

<stdint.h> 定義了 int16_t, uint32_t, int64_t 等整數, 在需要確保整數大小時可以使用它們代替 short, unsigned long long 等. 在 C 整型中, 只使用 int. 在合適的情況下, 推薦使用標準類型如 size_tptrdiff_t.

如果已知整數不會太大, 我們常常會使用 int, 如循環計數. 在類似的情況下使用原生類型 int. 你可以認為 int 至少為 32 位, 但不要認為它會多於 32 位. 如果需要 64 位整數, 用 int64_tuint64_t.

對於大整數, 使用 int64_t.

不要使用 uint32_t 等無符號整數, 除非你是在表示一個位組而不是一個數值, 或是你需要定義二進制補碼溢出. 尤其是不要為了指出數值永不會為負, 而使用無符號類型. 相反, 你應該使用斷言來保護數據.

如果你的程式碼涉及容器返回的大小(size),確保其類型足以應付容器各種可能的用法。拿不準時,類型越大越好。

小心整數類型轉換和整數提升(acgtyrant 註:integer promotions, 比如 intunsigned int 運算時,前者被提升為 unsigned int 而有可能溢出),總有意想不到的後果。

關於無符號整數:

有些人, 包括一些教科書作者, 推薦使用無符號類型表示非負數. 這種做法試圖達到自我文檔化. 但是, 在 C 語言中, 這一優點被由其導致的 bug 所淹沒. 看看下面的例子:

for (unsigned int i = foo.Length()-1; i >= 0; --i) ...

上述循環永遠不會退出! 有時 gcc 會發現該 bug 並報警, 但大部分情況下都不會. 類似的 bug 還會出現在比較有符合變數和無符號變量時. 主要是 C 的類型提升機制會致使無符號類型的行為出乎你的意料.

因此, 使用斷言來指出變數為非負數, 而不是使用無符號型!

5.15. 64 位下的可移植性

Tip

程式碼應該對 64 位和 32 位系統友好. 處理打印, 比較, 結構體對齊時應切記:

  • 對於某些類型, printf() 的指示符在 32 位和 64 位系統上可移植性不是很好. C99 標準定義了一些可移植的格式化指示符. 不幸的是, MSVC 7.1 並非全部支持, 而且標準中也有所遺漏, 所以有時我們不得不自己定義一個醜陋的版本 (標頭檔 inttypes.h 仿標準風格):

    // printf macros for size_t, in the style of inttypes.h
    #ifdef _LP64
    #define __PRIS_PREFIX "z"
    #else
    #define __PRIS_PREFIX
    #endif
    
    // Use these macros after a % in a printf format string
    // to get correct 32/64 bit behavior, like this:
    // size_t size = records.size();
    // printf("%"PRIuS"\n", size);
    #define PRIdS __PRIS_PREFIX "d"
    #define PRIxS __PRIS_PREFIX "x"
    #define PRIuS __PRIS_PREFIX "u"
    #define PRIXS __PRIS_PREFIX "X"
    #define PRIoS __PRIS_PREFIX "o"
    
    類型 不要使用 使用 備註
    void * (或其他指標類型) %lx %p  
    int64_t %qd, %lld %"PRId64"  
    uint64_t %qu, %llu, %llx %"PRIu64", %"PRIx64"  
    size_t %u %"PRIuS", %"PRIxS" C99 規定 %zu
    ptrdiff_t %d %"PRIdS" C99 規定 %zd

    注意 PRI* 巨集會被編譯器擴展為獨立字符串. 因此如果使用非常數的格式化字符串, 需要將宏的值而不是宏名插入格式中. 使用 PRI* 宏同樣可以在 % 後包含長度指示符. 例如, printf("x = %30"PRIuS"\n", x) 在 32 位 Linux 上將被展開為 printf("x = %30" "u" "\n", x), 編譯器當成 printf("x = %30u\n", x) 處理 (Yang.Y 注: 這在 MSVC 6.0 上行不通, VC 6 編譯器不會自動把引號間隔的多個字符串連接一個長字符串).

  • 記住 sizeof(void *) != sizeof(int). 如果需要一個指標大小的整數要用 intptr_t.

  • 你要非常小心的對待結構體對齊, 尤其是要持久化到磁盤上的結構體 (Yang.Y 注: 持久化 - 將數據按字節串流順序保存在磁盤文件或數據庫中). 在 64 位系統中, 任何含有 int64_t/uint64_t 成員的類/結構體, 缺省都以 8 字節在結尾對齊. 如果 32 位和 64 位程式碼要共用持久化的結構體, 需要確保兩種體系結構下的結構體對齊一致. 大多數編譯器都允許調整結構體對齊. gcc 中可使用 __attribute__((packed)). MSVC 則提供了 #pragma pack()__declspec(align()) (YuleFox 注, 解決方案的專案屬性裡也可以直接設置).

  • 創建 64 位常數時使用 LL 或 ULL 作為後綴, 如:

    int64_t my_value = 0×123456789LL;
    uint64_t my_mask = 3ULL << 48;
    
  • 如果你確實需要 32 位和 64 位系統具有不同程式碼, 可以使用 #ifdef _LP64 指令來切分 32/64 位程式碼. (盡量不要這麼做, 如果非用不可, 盡量使修改局部化)

5.16. 前處理巨集

Tip

使用巨集時要非常謹慎, 盡量以內聯函式, 列舉和常數代替之.

巨集意味著你和編譯器看到的程式碼是不同的. 這可能會導致例外行為, 尤其因為宏具有全域作用域.

值得慶幸的是, C++ 中, 巨集不像在 C 中那麼必不可少. 以往用宏展開性能關鍵的程式碼, 現在可以用內聯函式替代. 用宏表示常數可被 const 變數代替. 用宏 “縮寫” 長變量名可被引用代替. 用宏進行條件編譯... 這個, 千萬別這麼做, 會令測試更加痛苦 (#define 防止標頭檔重包含當然是個特例).

巨集可以做一些其他技術無法實作的事情, 在一些程式碼庫 (尤其是底層庫中) 可以看到宏的某些特性 (如用 # 字符串化, 用 ## 連接等等). 但在使用前, 仔細考慮一下能不能不使用宏達到同樣的目的.

下面給出的用法模式可以避免使用巨集帶來的問題; 如果你要宏, 盡可能遵守:

  • 不要在 .h 文件中定義巨集.
  • 在馬上要使用時才進行 #define, 使用後要立即 #undef.
  • 不要只是對已經存在的巨集使用#undef,選擇一個不會衝突的名稱;
  • 不要試圖使用展開後會導致 C++ 建構不穩定的巨集, 不然也至少要附上文檔說明其行為.
  • 不要用 ## 處理函式,類和變數的名字。

5.17. 0, nullptrNULL

Tip

整數用 0, 實數用 0.0, 指標用 nullptrNULL, 字符 (串) 用 '\0'.

整數用 0, 實數用 0.0, 這一點是毫無爭議的.

對於指標 (地址值), 到底是用 0, NULL 還是 nullptr. C++11 專案用 nullptr; C++03 專案則用 NULL, 畢竟它看起來像指針。實際上,一些 C++ 編譯器對 NULL 的定義比較特殊,可以輸出有用的警告,特別是 sizeof(NULL) 就和 sizeof(0) 不一樣。

字符 (串) 用 '\0', 不僅類型正確而且可讀性好.

5.18. sizeof

Tip

盡可能用 sizeof(varname) 代替 sizeof(type).

使用 sizeof(varname) 是因為當程式碼中變數類型改變時會自動更新. 你或許會用 sizeof(type) 處理不涉及任何變量的程式碼,比如處理來自外部或內部的數據格式,這時用變量就不合適了。

Struct data;
Struct data; memset(&data, 0, sizeof(data));

Warning

memset(&data, 0, sizeof(Struct));
if (raw_size < sizeof(int)) {
    LOG(ERROR) << "compressed record not big enough for count: " << raw_size;
    return false;
}

5.19. auto

Tip

auto 繞過煩瑣的類型名,只要可讀性好就繼續用,別用在區域變數之外的地方。

定義:

C++11 中,若變數被宣告成 auto, 那它的類型就會被自動匹配成初始化表達式的類型。你可以用 auto 來複製初始化或綁定引用。

vector<string> v;
...
auto s1 = v[0];  // 創建一份 v[0] 的拷貝。
const auto& s2 = v[0];  // s2 是 v[0] 的一個引用。

優點:

C++ 類型名有時又長又臭,特別是涉及樣板或命名空間的時候。就像:

sparse_hash_map<string, int>::iterator iter = m.find(val);

返回類型好難讀,程式碼目的也不夠一目瞭然。重構其:

auto iter = m.find(val);

好多了。

沒有 auto 的話,我們不得不在同一個表達式裡寫同一個類型名兩次,無謂的重複,就像:

diagnostics::ErrorStatus* status = new diagnostics::ErrorStatus("xyz");

有了 auto, 可以更方便地用中間變數,顯式編寫它們的類型輕鬆點。

缺點:

類型夠明顯時,特別是初始化變數時,程式碼才會夠一目瞭然。但以下就不一樣了:

auto i = x.Lookup(key);

看不出其類型是啥,x 的類型宣告恐怕遠在幾百行之外了。

開發者必須會區分 autoconst auto& 的不同之處,否則會複製錯東西。

auto 和 C++11 列表初始化的合體令人摸不著頭腦:

auto x(3);  // 圓括號。
auto y{3};  // 大括號。

它們不是同一回事——xint, y 則是 std::initializer_list<int>. 其它一般不可見的代理類型(acgtyrant 註:normally-invisible proxy types, 它涉及到 C++ 鮮為人知的坑:Why is vector<bool> not a STL container?)也有大同小異的陷阱。

如果在介面裡用 auto, 比如宣告標頭檔裡的一個常數,那麼只要僅僅因為開發者一時修改其值而導致類型變化的話——API 要翻天覆地了。

結論:

auto 只能用在區域變數裡用。別用在文件作用域變量,命名空間作用域變量和類數據成員裡。永遠別列表初始化 auto 變量。

auto 還可以和 C++11 特性「尾置返回類型(trailing return type)」一起用,不過後者只能用在 lambda 表達式裡。

5.20. 列表初始化

Tip

你可以用列表初始化。

早在 C++03 裡,聚合類型(aggregate types)就已經可以被列表初始化了,比如陣列和不自帶建構子的結構體:

struct Point { int x; int y; };
Point p = {1, 2};

C++11 中,該特性得到進一步的推廣,任何對象類型都可以被列表初始化。示範如下:

// Vector 接收了一個初始化列表。
vector<string> v{"foo", "bar"};

// 不考慮細節上的微妙差別,大致上相同。
// 你可以任選其一。
vector<string> v = {"foo", "bar"};

// 可以配合 new 一起用。
auto p = new vector<string>{"foo", "bar"};

// map 接收了一些 pair, 列表初始化大顯神威。
map<int, string> m = {{1, "one"}, {2, "2"}};

// 初始化列表也可以用在返回類型上的隱式轉換。
vector<int> test_function() { return {1, 2, 3}; }

// 初始化列表可迭代。
for (int i : {-1, -2, -3}) {}

// 在函式呼叫裡用列表初始化。
void TestFunction2(vector<int> v) {}
TestFunction2({1, 2, 3});

使用者自定義類型也可以定義接收 std::initializer_list<T> 的建構子和賦值運算子,以自動列表初始化:

class MyType {
 public:
  // std::initializer_list 專門接收 init 列表。
  // 得以值傳遞。
  MyType(std::initializer_list<int> init_list) {
    for (int i : init_list) append(i);
  }
  MyType& operator=(std::initializer_list<int> init_list) {
    clear();
    for (int i : init_list) append(i);
  }
};
MyType m{2, 3, 5, 7};

最後,列表初始化也適用於常規數據類型的建構,哪怕沒有接收 std::initializer_list<T> 的建構子。

double d{1.23};
// MyOtherType 沒有 std::initializer_list 建構子,
 // 直接上接收常規類型的建構子。
class MyOtherType {
 public:
  explicit MyOtherType(string);
  MyOtherType(int, string);
};
MyOtherType m = {1, "b"};
// 不過如果建構子是顯式的(explict),你就不能用 `= {}` 了。
MyOtherType m{"b"};

千萬別直接列表初始化 auto 變數,看下一句,估計沒人看得懂:

Warning

auto d = {1.23};        // d 即是 std::initializer_list<double>
auto d = double{1.23};  // 善哉 -- d 即為 double, 並非 std::initializer_list.

至於格式化,參見 braced-initializer-list-format.

5.21. Lambda 表達式

Tip

適當使用 lambda 表達式。別用默認 lambda 捕獲,所有捕獲都要顯式寫出來。

定義:

Lambda 表達式是創建匿名函式對象的一種簡易途徑,常用於把函式當參數傳,例如:

std::sort(v.begin(), v.end(), [](int x, int y) {
    return Weight(x) < Weight(y);
});

C++11 首次提出 Lambdas, 還提供了一系列處理函式對象的工具,比如多態包裝器(polymorphic wrapper) std::function.

優點:

  • 傳函式對象給 STL 算法,Lambdas 最簡易,可讀性也好。
  • Lambdas, std::functionsstd::bind 可以搭配成通用回調機制(general purpose callback mechanism);寫接收有界函式為參數的函式也很容易了。

缺點:

  • Lambdas 的變數捕獲略旁門左道,可能會造成懸空指標。
  • Lambdas 可能會失控;層層嵌套的匿名函式難以閱讀。

結論:

  • 按 format 小用 lambda 表達式怡情。
  • 禁用默認捕獲,捕獲都要顯式寫出來。打比方,比起 [=](int x) {return x + n;}, 你該寫成 [n](int x) {return x + n;} 才對,這樣讀者也好一眼看出 n 是被捕獲的值。
  • 匿名函式始終要簡短,如果函式體超過了五行,那麼還不如起名(acgtyrant 註:即把 lambda 表達式賦值給對象),或改用函式。
  • 如果可讀性更好,就顯式寫出 lambd 的尾置返回類型,就像auto.

5.22. 樣板元程式撰寫

TODO

5.23. Boost 庫

Tip

只使用 Boost 中被認可的庫.

定義:

Boost 庫集 是一個廣受歡迎, 經過同行鑒定, 免費開源的 C++ 庫集.

優點:

Boost程式碼質量普遍較高, 可移植性好, 填補了 C++ 標準函式庫很多空白, 如型別的特性, 更完善的綁定器, 更好的智慧指標。

缺點:

某些 Boost 庫提倡的程式撰寫實踐可讀性差, 比如元程式撰寫和其他高級樣板技術, 以及過度 “函式化” 的程式撰寫風格.

結論:

為了向閱讀和維護程式碼的人員提供更好的可讀性, 我們只允許使用 Boost 一部分經認可的特性子集. 目前允許使用以下庫:

  • Call Traits : boost/call_traits.hpp
  • Compressed Pair : boost/compressed_pair.hpp
  • <The Boost Graph Library (BGL) : boost/graph, except serialization (adj_list_serialize.hpp) and parallel/distributed algorithms and data structures(boost/graph/parallel/* and boost/graph/distributed/*)
  • Property Map : boost/property_map.hpp
  • The part of Iterator that deals with defining iterators: boost/iterator/iterator_adaptor.hpp, boost/iterator/iterator_facade.hpp, and boost/function_output_iterator.hpp
  • The part of Polygon that deals with Voronoi diagram construction and doesn’t depend on the rest of Polygon: boost/polygon/voronoi_builder.hpp, boost/polygon/voronoi_diagram.hpp, and boost/polygon/voronoi_geometry_type.hpp
  • Bimap : boost/bimap
  • Statistical Distributions and Functions : boost/math/distributions
  • Multi-index : boost/multi_index
  • Heap : boost/heap
  • The flat containers from Container: boost/container/flat_map, and boost/container/flat_set

我們正在積極考慮增加其它 Boost 特性, 所以列表中的規則將不斷變化.

以下庫可以用,但由於如今已經被 C++ 11 標準函式庫取代,不再鼓勵:

5.24. C++11

Tip

適當用 C++11(前身是 C++0x)的庫和語言擴展,在貴專案用 C++11 特性前三思可移植性。

定義:

C++11 有眾多語言和庫上的`變革 <https://en.wikipedia.org/wiki/C%2B%2B11>`_。

優點:

在二一四年八月之前,C++11 一度是官方標準,被大多 C++ 編譯器支持。它標準化很多我們早先就在用的 C++ 擴展,簡化了不少操作,大大改善了性能和安全。

缺點:

C++11 相對於前身,複雜極了:1300 頁 vs 800 頁!很多開發者也不怎麼熟悉它。於是從長遠來看,前者特性對程式碼可讀性以及維護代價難以預估。我們說不准什麼時候採納其特性,特別是在被迫相依性老實工具的專案上。

5.23. Boost 庫 一樣,有些 C++11 擴展提倡實則對可讀性有害的程式撰寫實踐——就像去除冗余檢查(比如類型名)以幫助讀者,或是鼓勵樣板元程式撰寫等等。有些擴展在功能上與原有機制衝突,容易招致困惑以及遷移代價。

缺點:

C++11 特性除了個別情況下,可以用一用。除了本指南會有不少章節會加以討若干 C++11 特性之外,以下特性最好不要用:

  • 尾置返回類型,比如用 auto foo() -> int 代替 int foo(). 為了兼容於現有程式碼的宣告風格。
  • 編譯時合數 <ratio>, 因為它涉及一個重樣板的介面風格。
  • <cfenv>``<fenv.h>` 標頭檔,因為編譯器尚不支持。
  • 默認 lambda 捕獲。

譯者(acgtyrant)筆記

  1. 實際上,預設參數會改變函式簽名的前提是改變了它接收的參數數量,比如把 void a() 改成 void a(int b = 0), 開發者改變其程式碼的初衷也許是,在不改變「程式碼相容性」的同時,又提供了可選 int 參數的餘地,然而這終究會破壞函式指標上的相容性,畢竟函式簽名確實變了。
  2. 此外把自帶預設參數的函式地址賦值給指標時,會丟失缺省參數信息。
  3. 我還發現 濫用預設參數會害得讀者光只看呼叫程式碼的話,會誤以為其函式接受的參數數量比實際上還要少。
  4. friend 實際上只對函式/類賦予了對其所在類的訪問權限,並不是有效的宣告語句。所以除了在標頭檔類內部寫 friend 函式/類,還要在類作用域之外正式地宣告一遍,最後在對應的 .cc 文件加以定義。
  5. 本風格指南都強調了「友元應該定義在同一文件內,避免程式碼讀者跑到其它文件查找使用該私有成員的類」。那麼可以把其宣告放在類宣告所在的標頭檔,定義也放在類定義所在的文件。
  6. 由於友元函式/類並不是類的一部分,自然也不會是類可呼叫的公有介面,於是我主張全集中放在類的尾部,即 :ref:`private 的數據成員 <declaration-order>`_ 之後。
  7. 對使用 C++ 例外處理應具有怎樣的態度? 非常值得一讀。
  8. 注意初始化 const 對象時,必須在初始化的同時值初始化。
  9. 用斷言代替無符號整數類型,深有啟發。
  10. auto 在涉及迭代器的循環語句裡挺常用。
  11. Should the trailing return type syntax style become the default for new C++11 programs? 討論了 auto 與尾置返回類型一起用的全新編碼風格,值得一看。