6. 其他 C++ 特性
6.1. 引用參數
Tip
所有按引用傳遞的參數必須加上const.
定義:
在 C 語言中, 如果函數需要修改變量的值, 參數必須為指針, 如intfoo(int*pval). 在 C++ 中, 函數還可以聲明引用參數:intfoo(int&val).
優點:
定義引用參數防止出現(*pval)++這樣丑陋的代碼. 像拷貝構造函數這樣的應用也是必需的. 而且更明確, 不接受NULL指針.
缺點:
容易引起誤解, 因為引用在語法上是值變量卻擁有指針的語義.
結論:
函數參數列表中, 所有引用參數都必須是const:
void Foo(const string &in, string *out);
事實上這在 Google Code 是一個硬性約定: 輸入參數是值參或const引用, 輸出參數為指針. 輸入參數可以是const指針, 但決不能是非const的引用參數,除非用于交換,比如swap().
有時候,在輸入形參中用constT*指針比constT&更明智。比如:
您會傳 null 指針。
函數要把指針或對地址的引用賦值給輸入形參。
總之大多時候輸入形參往往是constT&. 若用constT*說明輸入另有處理。所以若您要用constT*, 則應有理有據,否則會害得讀者誤解。
6.2. 右值引用
Tip
只在定義移動構造函數與移動賦值操作時使用右值引用. 不要使用std::forward.
定義:
右值引用是一種只能綁定到臨時對象的引用的一種, 其語法與傳統的引用語法相似. 例如,voidf(string&&s); 聲明了一個其參數是一個字符串的右值引用的函數.
優點:
用于定義移動構造函數 (使用類的右值引用進行構造的函數) 使得移動一個值而非拷貝之成為可能. 例如, 如果v1是一個vector
右值引用使得編寫通用的函數封裝來轉發其參數到另外一個函數成為可能, 無論其參數是否是臨時對象都能正常工作.
右值引用能實現可移動但不可拷貝的類型, 這一特性對那些在拷貝方面沒有實際需求, 但有時又需要將它們作為函數參數傳遞或塞入容器的類型很有用.
要高效率地使用某些標準庫類型, 例如std::unique_ptr,std::move是必需的.
缺點:
右值引用是一個相對比較新的特性 (由 C++11 引入), 它尚未被廣泛理解. 類似引用崩潰, 移動構造函數的自動推導這樣的規則都是很復雜的.
結論:
只在定義移動構造函數與移動賦值操作時使用右值引用, 不要使用std::forward功能函數. 你可能會使用std::move來表示將值從一個對象移動而不是復制到另一個對象.
6.3. 函數重載
Tip
若要用好函數重載,最好能讓讀者一看調用點(call site)就胸有成竹,不用花心思猜測調用的重載函數到底是哪一種。該規則適用于構造函數。
定義:
你可以編寫一個參數類型為conststring&的函數, 然后用另一個參數類型為constchar*的函數重載它:
class MyClass { public: void Analyze(const string &text); void Analyze(const char *text, size_t textlen);};
優點:
通過重載參數不同的同名函數, 令代碼更加直觀. 模板化代碼需要重載, 同時為使用者帶來便利.
缺點:
如果函數單單靠不同的參數類型而重載(acgtyrant 注:這意味著參數數量不變),讀者就得十分熟悉 C++ 五花八門的匹配規則,以了解匹配過程具體到底如何。另外,當派生類只重載了某個函數的部分變體,繼承語義容易令人困惑。
結論:
如果您打算重載一個函數, 可以試試改在函數名里加上參數信息。例如,用AppendString()和AppendInt()等, 而不是一口氣重載多個Append().
6.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);
6.5. 變長數組和 alloca()
Tip
我們不允許使用變長數組和alloca().
優點:
變長數組具有渾然天成的語法. 變長數組和alloca()也都很高效.
缺點:
變長數組和alloca()不是標準 C++ 的組成部分. 更重要的是, 它們根據數據大小動態分配堆棧內存, 會引起難以發現的內存越界 bugs: “在我的機器上運行的好好的, 發布后卻莫名其妙的掛掉了”.
結論:
改用更安全的分配器(allocator),就像std::vector或std::unique_ptr
6.6. 友元
Tip
我們允許合理的使用友元類及友元函數.
通常友元應該定義在同一文件內, 避免代碼讀者跑到其它文件查找使用該私有成員的類. 經常用到友元的一個地方是將FooBuilder聲明為Foo的友元, 以便FooBuilder正確構造Foo的內部狀態, 而無需將該狀態暴露出來. 某些情況下, 將一個單元測試類聲明成待測類的友元會很方便.
友元擴大了 (但沒有打破) 類的封裝邊界. 某些情況下, 相對于將類成員聲明為public, 使用友元是更好的選擇, 尤其是如果你只允許另一個類訪問該類的私有成員時. 當然, 大多數類都只應該通過其提供的公有成員進行互操作.
6.7. 異常
Tip
我們不使用 C++ 異常.
優點:
異常允許應用高層決定如何處理在底層嵌套函數中「不可能發生」的失?。╢ailures),不用管那些含糊且容易出錯的錯誤代碼(acgtyrant 注:error code, 我猜是C語言函數返回的非零 int 值)。
很多現代語言都用異常。引入異常使得 C++ 與 Python, Java 以及其它類 C++ 的語言更一脈相承。
有些第三方 C++ 庫依賴異常,禁用異常就不好用了。
異常是處理構造函數失敗的唯一途徑。雖然可以用工廠函數(acgtyrant 注:factory function, 出自 C++ 的一種設計模式,即「簡單工廠模式」)或Init()方法代替異常, 但是前者要求在堆棧分配內存,后者會導致剛創建的實例處于 ”無效“ 狀態。
在測試框架里很好用。
缺點:
在現有函數中添加throw語句時,您必須檢查所有調用點。要么讓所有調用點統統具備最低限度的異常安全保證,要么眼睜睜地看異常一路歡快地往上跑,最終中斷掉整個程序。舉例,f()調用g(),g()又調用h(), 且h拋出的異常被f捕獲。當心g, 否則會沒妥善清理好。
還有更常見的,異常會徹底擾亂程序的執行流程并難以判斷,函數也許會在您意料不到的地方返回。您或許會加一大堆何時何處處理異常的規定來降低風險,然而開發者的記憶負擔更重了。
異常安全需要RAII和不同的編碼實踐. 要輕松編寫出正確的異常安全代碼需要大量的支持機制. 更進一步地說, 為了避免讀者理解整個調用表, 異常安全必須隔絕從持續狀態寫到 “提交” 狀態的邏輯. 這一點有利有弊 (因為你也許不得不為了隔離提交而混淆代碼). 如果允許使用異常, 我們就不得不時刻關注這樣的弊端, 即使有時它們并不值得.
啟用異常會增加二進制文件數據,延長編譯時間(或許影響小),還可能加大地址空間的壓力。
濫用異常會變相鼓勵開發者去捕捉不合時宜,或本來就已經沒法恢復的「偽異?!?。比如,用戶的輸入不符合格式要求時,也用不著拋異常。如此之類的偽異常列都列不完。
結論:
從表面上看來,使用異常利大于弊, 尤其是在新項目中. 但是對于現有代碼, 引入異常會牽連到所有相關代碼. 如果新項目允許異常向外擴散, 在跟以前未使用異常的代碼整合時也將是個麻煩. 因為 Google 現有的大多數 C++ 代碼都沒有異常處理, 引入帶有異常處理的新代碼相當困難.
鑒于 Google 現有代碼不接受異常, 在現有代碼中使用異常比在新項目中使用的代價多少要大一些. 遷移過程比較慢, 也容易出錯. 我們不相信異常的使用有效替代方案, 如錯誤代碼, 斷言等會造成嚴重負擔.
我們并不是基于哲學或道德層面反對使用異常, 而是在實踐的基礎上. 我們希望在 Google 使用我們自己的開源項目, 但項目中使用異常會為此帶來不便, 因此我們也建議不要在 Google 的開源項目中使用異常. 如果我們需要把這些項目推倒重來顯然不太現實.
對于 Windows 代碼來說, 有個特例.
(YuleFox 注: 對于異常處理, 顯然不是短短幾句話能夠說清楚的, 以構造函數為例, 很多 C++ 書籍上都提到當構造失敗時只有異??梢蕴幚? Google 禁止使用異常這一點, 僅僅是為了自身的方便, 說大了, 無非是基于軟件管理成本上, 實際使用中還是自己決定)
6.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
缺點:
在運行時判斷類型通常意味著設計問題. 如果你需要在運行期間確定一個對象的類型, 這通常說明你需要考慮重新設計你的類.
隨意地使用 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 的理由同樣適用于這些方案, 比如帶類型標簽的類繼承體系. 而且, 這些方案會掩蓋你的真實意圖.
6.9. 類型轉換
Tip
使用 C++ 的類型轉換, 如static_cast<>(). 不要使用inty=(int)x或inty=int(x)等轉換方式;
定義:
C++ 采用了有別于 C 的類型轉換機制, 對轉換操作進行歸類.
優點:
C 語言的類型轉換問題在于模棱兩可的操作; 有時是在做強制轉換 (如(int)3.5), 有時是在做類型轉換 (如(int)"hello"). 另外, C++ 的類型轉換在查找時更醒目.
缺點:
惡心的語法.
結論:
不要使用 C 風格類型轉換. 而應該使用 C++ 風格.
用static_cast替代 C 風格的值轉換, 或某個類指針需要明確的向上轉換為父類指針時.
用const_cast去掉const限定符.
用reinterpret_cast指針類型和整型或其它指針之間進行不安全的相互轉換. 僅在你對所做一切了然于心時使用.
至于dynamic_cast參見6.8. 運行時類型識別.
6.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.
6.11. 前置自增和自減
Tip
對于迭代器和其他模板對象使用前綴形式 (++i) 的自增, 自減運算符.
定義:
對于變量在自增 (++i或i++) 或自減 (--i或i--) 后表達式的值又沒有沒用到的情況下, 需要確定到底是使用前置還是后置的自增 (自減).
優點:
不考慮返回值的話, 前置自增 (++i) 通常要比后置自增 (i++) 效率更高. 因為后置自增 (或自減) 需要對表達式的值i進行一次拷貝. 如果i是迭代器或其他非數值類型, 拷貝的代價是比較大的. 既然兩種自增方式實現的功能一樣, 為什么不總是使用前置自增呢?
缺點:
在 C 開發中, 當表達式的值未被使用時, 傳統的做法是使用后置自增, 特別是在for循環中. 有些人覺得后置自增更加易懂, 因為這很像自然語言, 主語 (i) 在謂語動詞 (++) 前.
結論:
對簡單數值 (非對象), 兩種都無所謂. 對迭代器和模板類型, 使用前置自增 (自減).
6.12.const用法
Tip
我們強烈建議你在任何可能的情況下都要使用const. 此外有時改用 C++11 推出的 constexpr 更好。
定義:
在聲明的變量或參數前加上關鍵字const用于指明變量值不可被篡改 (如constintfoo). 為類中的函數加上const限定符表明該函數不會修改類成員變量的狀態 (如classFoo{intBar(charc)const;};).
優點:
大家更容易理解如何使用變量. 編譯器可以更好地進行類型檢測, 相應地, 也能生成更好的代碼. 人們對編寫正確的代碼更加自信, 因為他們知道所調用的函數被限定了能或不能修改變量值. 即使是在無鎖的多線程編程中, 人們也知道什么樣的函數是安全的.
缺點:
const是入侵性的: 如果你向一個函數傳入const變量, 函數原型聲明中也必須對應const參數 (否則變量需要const_cast類型轉換), 在調用庫函數時顯得尤其麻煩.
結論:
const變量, 數據成員, 函數和參數為編譯時類型檢測增加了一層保障; 便于盡早發現錯誤. 因此, 我們強烈建議在任何可能的情況下使用const:
如果函數不會修改傳你入的引用或指針類型參數, 該參數應聲明為const.
盡可能將函數聲明為const. 訪問函數應該總是const. 其他不會修改任何數據成員, 未調用非const函數, 不會返回數據成員非const指針或引用的函數也應該聲明成const.
如果數據成員在對象構造之后不再發生變化, 可將其定義為const.
然而, 也不要發了瘋似的使用const. 像constint*const*constx;就有些過了, 雖然它非常精確的描述了常量x. 關注真正有幫助意義的信息: 前面的例子寫成constint**x就夠了.
關鍵字mutable可以使用, 但是在多線程中是不安全的, 使用時首先要考慮線程安全.
const的位置:
有人喜歡intconst*foo形式, 不喜歡constint*foo, 他們認為前者更一致因此可讀性也更好: 遵循了const總位于其描述的對象之后的原則. 但是一致性原則不適用于此, “不要過度使用” 的聲明可以取消大部分你原本想保持的一致性. 將const放在前面才更易讀, 因為在自然語言中形容詞 (const) 是在名詞 (int) 之前.
這是說, 我們提倡但不強制const在前. 但要保持代碼的一致性! (Yang.Y 注: 也就是不要在一些地方把const寫在類型前面, 在其他地方又寫在后面, 確定一種寫法, 然后保持一致.)
6.13.constexpr用法
Tip
在 C++11 里,用 constexpr 來定義真正的常量,或實現常量初始化。
定義:
變量可以被聲明成 constexpr 以表示它是真正意義上的常量,即在編譯時和運行時都不變。函數或構造函數也可以被聲明成 constexpr, 以用來定義 constexpr 變量。
優點:
如今 constexpr 就可以定義浮點式的真?常量,不用再依賴字面值了;也可以定義用戶自定義類型上的常量;甚至也可以定義函數調用所返回的常量。
缺點:
若過早把變量優化成 constexpr 變量,將來又要把它改為常規變量時,挺麻煩的;當前對constexpr函數和構造函數中允許的限制可能會導致這些定義中解決的方法模糊。
結論:
靠 constexpr 特性,方才實現了 C++ 在接口上打造真正常量機制的可能。好好用 constexpr 來定義真?常量以及支持常量的函數。避免復雜的函數定義,以使其能夠與constexpr一起使用。 千萬別癡心妄想地想靠 constexpr 來強制代碼「內聯」。
6.14. 整型
Tip
C++ 內建整型中, 僅使用int. 如果程序中需要不同大小的變量, 可以使用
定義:
C++ 沒有指定整型的大小. 通常人們假定short是 16 位,int是 32 位,long是 32 位,longlong是 64 位.
優點:
保持聲明統一.
缺點:
C++ 中整型大小因編譯器和體系結構的不同而不同.
結論:
如果已知整數不會太大, 我們常常會使用int, 如循環計數. 在類似的情況下使用原生類型int. 你可以認為int至少為 32 位, 但不要認為它會多于32位. 如果需要 64 位整型, 用int64_t或uint64_t.
對于大整數, 使用int64_t.
不要使用uint32_t等無符號整型, 除非你是在表示一個位組而不是一個數值, 或是你需要定義二進制補碼溢出. 尤其是不要為了指出數值永不會為負, 而使用無符號類型. 相反, 你應該使用斷言來保護數據.
如果您的代碼涉及容器返回的大?。╯ize),確保其類型足以應付容器各種可能的用法。拿不準時,類型越大越好。
小心整型類型轉換和整型提升(acgtyrant 注:integer promotions, 比如int與unsignedint運算時,前者被提升為unsignedint而有可能溢出),總有意想不到的后果。
關于無符號整數:
有些人, 包括一些教科書作者, 推薦使用無符號類型表示非負數. 這種做法試圖達到自我文檔化. 但是, 在 C 語言中, 這一優點被由其導致的 bug 所淹沒. 看看下面的例子:
for (unsigned int i = foo.Length()-1; i >= 0; --i) ...
上述循環永遠不會退出! 有時 gcc 會發現該 bug 并報警, 但大部分情況下都不會. 類似的 bug 還會出現在比較有符合變量和無符號變量時. 主要是 C 的類型提升機制會致使無符號類型的行為出乎你的意料.
因此, 使用斷言來指出變量為非負數, 而不是使用無符號型!
6.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 則提供了#pragmapack()和__declspec(align())(YuleFox 注, 解決方案的項目屬性里也可以直接設置).
創建 64 位常量時使用 LL 或 ULL 作為后綴, 如:
int64_t my_value = 0x123456789LL;uint64_t my_mask = 3ULL << 48;
如果你確實需要 32 位和 64 位系統具有不同代碼, 可以使用#ifdef_LP64指令來切分 32/64 位代碼. (盡量不要這么做, 如果非用不可, 盡量使修改局部化)
6.16. 預處理宏
Tip
使用宏時要非常謹慎, 盡量以內聯函數, 枚舉和常量代替之.
宏意味著你和編譯器看到的代碼是不同的. 這可能會導致異常行為, 尤其因為宏具有全局作用域.
值得慶幸的是, C++ 中, 宏不像在 C 中那么必不可少. 以往用宏展開性能關鍵的代碼, 現在可以用內聯函數替代. 用宏表示常量可被const變量代替. 用宏 “縮寫” 長變量名可被引用代替. 用宏進行條件編譯… 這個, 千萬別這么做, 會令測試更加痛苦 (#define防止頭文件重包含當然是個特例).
宏可以做一些其他技術無法實現的事情, 在一些代碼庫 (尤其是底層庫中) 可以看到宏的某些特性 (如用#字符串化, 用##連接等等). 但在使用前, 仔細考慮一下能不能不使用宏達到同樣的目的.
下面給出的用法模式可以避免使用宏帶來的問題; 如果你要宏, 盡可能遵守:
不要在.h文件中定義宏.
在馬上要使用時才進行#define, 使用后要立即#undef.
不要只是對已經存在的宏使用#undef,選擇一個不會沖突的名稱;
不要試圖使用展開后會導致 C++ 構造不穩定的宏, 不然也至少要附上文檔說明其行為.
不要用##處理函數,類和變量的名字。
6.17. 0,nullptr和NULL
Tip
整數用0, 實數用0.0, 指針用nullptr或NULL, 字符 (串) 用'\0'.
整數用0, 實數用0.0, 這一點是毫無爭議的.
對于指針 (地址值), 到底是用0,NULL還是nullptr. C++11 項目用nullptr; C++03 項目則用NULL, 畢竟它看起來像指針。實際上,一些 C++ 編譯器對NULL的定義比較特殊,可以輸出有用的警告,特別是sizeof(NULL)就和sizeof(0)不一樣。
字符 (串) 用'\0', 不僅類型正確而且可讀性好.
6.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;}
6.19. auto
Tip
用auto繞過煩瑣的類型名,只要可讀性好就繼續用,別用在局部變量之外的地方。
定義:
C++11 中,若變量被聲明成auto, 那它的類型就會被自動匹配成初始化表達式的類型。您可以用auto來復制初始化或綁定引用。
vector
優點:
C++ 類型名有時又長又臭,特別是涉及模板或命名空間的時候。就像:
sparse_hash_map
返回類型好難讀,代碼目的也不夠一目了然。重構其:
auto iter = m.find(val);
好多了。
沒有auto的話,我們不得不在同一個表達式里寫同一個類型名兩次,無謂的重復,就像:
diagnostics::ErrorStatus* status = new diagnostics::ErrorStatus("xyz");
有了 auto, 可以更方便地用中間變量,顯式編寫它們的類型輕松點。
缺點:
類型夠明顯時,特別是初始化變量時,代碼才會夠一目了然。但以下就不一樣了:
auto i = x.Lookup(key);
看不出其類型是啥,x 的類型聲明恐怕遠在幾百行之外了。
程序員必須會區分auto和constauto&的不同之處,否則會復制錯東西。
auto 和 C++11 列表初始化的合體令人摸不著頭腦:
auto x(3); // 圓括號。auto y{3}; // 大括號。
它們不是同一回事——x是int,y則是std::initializer_list
如果在接口里用auto, 比如聲明頭文件里的一個常量,那么只要僅僅因為程序員一時修改其值而導致類型變化的話——API 要翻天覆地了。
結論:
auto只能用在局部變量里用。別用在文件作用域變量,命名空間作用域變量和類數據成員里。永遠別列表初始化auto變量。
auto還可以和 C++11 特性「尾置返回類型(trailing return type)」一起用,不過后者只能用在 lambda 表達式里。
6.20. 列表初始化
Tip
你可以用列表初始化。
早在 C++03 里,聚合類型(aggregate types)就已經可以被列表初始化了,比如數組和不自帶構造函數的結構體:
struct Point { int x; int y; };Point p = {1, 2};
C++11 中,該特性得到進一步的推廣,任何對象類型都可以被列表初始化。示范如下:
// Vector 接收了一個初始化列表。vector
用戶自定義類型也可以定義接收std::initializer_list
class MyType { public: // std::initializer_list 專門接收 init 列表。 // 得以值傳遞。 MyType(std::initializer_list
最后,列表初始化也適用于常規數據類型的構造,哪怕沒有接收std::initializer_list
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
至于格式化,參見9.7. 列表初始化格式.
6.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::functions和std::bind可以搭配成通用回調機制(general purpose callback mechanism);寫接收有界函數為參數的函數也很容易了。
缺點:
Lambdas 的變量捕獲略旁門左道,可能會造成懸空指針。
Lambdas 可能會失控;層層嵌套的匿名函數難以閱讀。
結論:
按 format 小用 lambda 表達式怡情。
禁用默認捕獲,捕獲都要顯式寫出來。打比方,比起[=](intx){returnx+n;}, 您該寫成[n](intx){returnx+n;}才對,這樣讀者也好一眼看出n是被捕獲的值。
匿名函數始終要簡短,如果函數體超過了五行,那么還不如起名(acgtyrant 注:即把 lambda 表達式賦值給對象),或改用函數。
如果可讀性更好,就顯式寫出 lambd 的尾置返回類型,就像auto.
6.22. 模板編程
Tip
不要使用復雜的模板編程
定義:
模板編程指的是利用c++ 模板實例化機制是圖靈完備性, 可以被用來實現編譯時刻的類型判斷的一系列編程技巧
優點:
模板編程能夠實現非常靈活的類型安全的接口和極好的性能, 一些常見的工具比如Google Test, std::tuple, std::function 和 Boost.Spirit. 這些工具如果沒有模板是實現不了的
缺點:
模板編程所使用的技巧對于使用c++不是很熟練的人是比較晦澀, 難懂的. 在復雜的地方使用模板的代碼讓人更不容易讀懂, 并且debug 和 維護起來都很麻煩
模板編程經常會導致編譯出錯的信息非常不友好: 在代碼出錯的時候, 即使這個接口非常的簡單, 模板內部復雜的實現細節也會在出錯信息顯示. 導致這個編譯出錯信息看起來非常難以理解.
大量的使用模板編程接口會讓重構工具(Visual Assist X, Refactor for C++等等)更難發揮用途. 首先模板的代碼會在很多上下文里面擴展開來, 所以很難確認重構對所有的這些展開的代碼有用, 其次有些重構工具只對已經做過模板類型替換的代碼的AST 有用. 因此重構工具對這些模板實現的原始代碼并不有效, 很難找出哪些需要重構.
結論:
模板編程有時候能夠實現更簡潔更易用的接口, 但是更多的時候卻適得其反. 因此模板編程最好只用在少量的基礎組件, 基礎數據結構上, 因為模板帶來的額外的維護成本會被大量的使用給分擔掉
在使用模板編程或者其他復雜的模板技巧的時候, 你一定要再三考慮一下. 考慮一下你們團隊成員的平均水平是否能夠讀懂并且能夠維護你寫的模板代碼.或者一個非c++ 程序員和一些只是在出錯的時候偶爾看一下代碼的人能夠讀懂這些錯誤信息或者能夠跟蹤函數的調用流程. 如果你使用遞歸的模板實例化, 或者類型列表, 或者元函數, 又或者表達式模板, 或者依賴SFINAE, 或者sizeof 的trick 手段來檢查函數是否重載, 那么這說明你模板用的太多了, 這些模板太復雜了, 我們不推薦使用
如果你使用模板編程, 你必須考慮盡可能的把復雜度最小化, 并且盡量不要讓模板對外暴漏. 你最好只在實現里面使用模板, 然后給用戶暴露的接口里面并不使用模板, 這樣能提高你的接口的可讀性. 并且你應該在這些使用模板的代碼上寫盡可能詳細的注釋. 你的注釋里面應該詳細的包含這些代碼是怎么用的, 這些模板生成出來的代碼大概是什么樣子的. 還需要額外注意在用戶錯誤使用你的模板代碼的時候需要輸出更人性化的出錯信息. 因為這些出錯信息也是你的接口的一部分, 所以你的代碼必須調整到這些錯誤信息在用戶看起來應該是非常容易理解, 并且用戶很容易知道如何修改這些錯誤
6.23. Boost 庫
Tip
只使用 Boost 中被認可的庫.
定義:
Boost 庫集是一個廣受歡迎, 經過同行鑒定, 免費開源的 C++ 庫集.
優點:
Boost代碼質量普遍較高, 可移植性好, 填補了 C++ 標準庫很多空白, 如型別的特性, 更完善的綁定器, 更好的智能指針。
缺點:
某些 Boost 庫提倡的編程實踐可讀性差, 比如元編程和其他高級模板技術, 以及過度 “函數化” 的編程風格.
結論:
為了向閱讀和維護代碼的人員提供更好的可讀性, 我們只允許使用 Boost 一部分經認可的特性子集. 目前允許使用以下庫:
Call Traits:boost/call_traits.hpp
Compressed Pair:boost/compressed_pair.hpp
Property Map:boost/property_map.hpp
The part ofIteratorthat deals with defining iterators:boost/iterator/iterator_adaptor.hpp,boost/iterator/iterator_facade.hpp, andboost/function_output_iterator.hpp
The part ofPolygonthat deals with Voronoi diagram construction and doesn’t depend on the rest of Polygon:boost/polygon/voronoi_builder.hpp,boost/polygon/voronoi_diagram.hpp, andboost/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 fromContainer:boost/container/flat_map, andboost/container/flat_set
我們正在積極考慮增加其它 Boost 特性, 所以列表中的規則將不斷變化.
以下庫可以用,但由于如今已經被 C++ 11 標準庫取代,不再鼓勵:
Pointer Container:boost/ptr_container, 改用std::unique_ptr
Array:boost/array.hpp, 改用std::array
6.24. C++11
Tip
適當用 C++11(前身是 C++0x)的庫和語言擴展,在貴項目用 C++11 特性前三思可移植性。
定義:
C++11 有眾多語言和庫上的`變革
優點:
在二〇一四年八月之前,C++11 一度是官方標準,被大多 C++ 編譯器支持。它標準化很多我們早先就在用的 C++ 擴展,簡化了不少操作,大大改善了性能和安全。
缺點:
C++11 相對于前身,復雜極了:1300 頁 vs 800 頁!很多開發者也不怎么熟悉它。于是從長遠來看,前者特性對代碼可讀性以及維護代價難以預估。我們說不準什么時候采納其特性,特別是在被迫依賴老實工具的項目上。
和6.23. Boost 庫一樣,有些 C++11 擴展提倡實則對可讀性有害的編程實踐——就像去除冗余檢查(比如類型名)以幫助讀者,或是鼓勵模板元編程等等。有些擴展在功能上與原有機制沖突,容易招致困惑以及遷移代價。
缺點:
C++11 特性除了個別情況下,可以用一用。除了本指南會有不少章節會加以討若干 C++11 特性之外,以下特性最好不要用:
尾置返回類型,比如用autofoo()->int代替intfoo(). 為了兼容于現有代碼的聲明風格。
編譯時合數
默認 lambda 捕獲。
譯者(acgtyrant)筆記
實際上,缺省參數會改變函數簽名的前提是改變了它接收的參數數量,比如把voida()改成voida(intb=0), 開發者改變其代碼的初衷也許是,在不改變「代碼兼容性」的同時,又提供了可選 int 參數的余地,然而這終究會破壞函數指針上的兼容性,畢竟函數簽名確實變了。
此外把自帶缺省參數的函數地址賦值給指針時,會丟失缺省參數信息。
我還發現濫用缺省參數會害得讀者光只看調用代碼的話,會誤以為其函數接受的參數數量比實際上還要少。
friend實際上只對函數/類賦予了對其所在類的訪問權限,并不是有效的聲明語句。所以除了在頭文件類內部寫 friend 函數/類,還要在類作用域之外正式地聲明一遍,最后在對應的.cc文件加以定義。
本風格指南都強調了「友元應該定義在同一文件內,避免代碼讀者跑到其它文件查找使用該私有成員的類」。那么可以把其聲明放在類聲明所在的頭文件,定義也放在類定義所在的文件。
由于友元函數/類并不是類的一部分,自然也不會是類可調用的公有接口,于是我主張全集中放在類的尾部,即的數據成員之后,參考聲明順序。
對使用 C++ 異常處理應具有怎樣的態度?非常值得一讀。
注意初始化 const 對象時,必須在初始化的同時值初始化。
用斷言代替無符號整型類型,深有啟發。
auto 在涉及迭代器的循環語句里挺常用。
Should the trailing return type syntax style become the default for new C++11 programs?討論了 auto 與尾置返回類型一起用的全新編碼風格,值得一看。
-
Google
+關注
關注
5文章
1757瀏覽量
57412 -
編程
+關注
關注
88文章
3591瀏覽量
93592 -
C++
+關注
關注
22文章
2104瀏覽量
73487
原文標題:Google C++ 編程規范 - 4
文章出處:【微信號:C_Expert,微信公眾號:C語言專家集中營】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論