2. 作用域
2.1. 命名空間
Tip
鼓勵在.cc文件內使用匿名命名空間或static聲明. 使用具名的命名空間時, 其名稱可基于項目名或相對路徑. 禁止使用 using 指示(using-directive)。禁止使用內聯命名空間(inline namespace)。
定義:
命名空間將全局作用域細分為獨立的, 具名的作用域, 可有效防止全局作用域的命名沖突.
優點:
雖然類已經提供了(可嵌套的)命名軸線 (YuleFox 注: 將命名分割在不同類的作用域內), 命名空間在這基礎上又封裝了一層.
舉例來說, 兩個不同項目的全局作用域都有一個類Foo, 這樣在編譯或運行時造成沖突. 如果每個項目將代碼置于不同命名空間中,project1::Foo和project2::Foo作為不同符號自然不會沖突.
內聯命名空間會自動把內部的標識符放到外層作用域,比如:
namespace X {inline namespace Y {void foo();} // namespace Y} // namespace X
X::Y::foo()與X::foo()彼此可代替。內聯命名空間主要用來保持跨版本的 ABI 兼容性。
缺點:
命名空間具有迷惑性, 因為它們使得區分兩個相同命名所指代的定義更加困難。
內聯命名空間很容易令人迷惑,畢竟其內部的成員不再受其聲明所在命名空間的限制。內聯命名空間只在大型版本控制里有用。
有時候不得不多次引用某個定義在許多嵌套命名空間里的實體,使用完整的命名空間會導致代碼的冗長。
在頭文件中使用匿名空間導致違背 C++ 的唯一定義原則 (One Definition Rule (ODR)).
結論:
根據下文將要提到的策略合理使用命名空間.
遵守命名空間命名中的規則。
像之前的幾個例子中一樣,在命名空間的最后注釋出命名空間的名字。
用命名空間把文件包含,gflags的聲明/定義, 以及類的前置聲明以外的整個源文件封裝起來, 以區別于其它命名空間:
// .h 文件namespace mynamespace {// 所有聲明都置于命名空間中// 注意不要使用縮進class MyClass { public: ... void Foo();};} // namespace mynamespace// .cc 文件namespace mynamespace {// 函數定義都置于命名空間中void MyClass::Foo() { ...}} // namespace mynamespace
更復雜的.cc文件包含更多, 更復雜的細節, 比如 gflags 或 using 聲明。
#include "a.h"DEFINE_FLAG(bool, someflag, false, "dummy flag");namespace a {...code for a... // 左對齊} // namespace a
不要在命名空間std內聲明任何東西, 包括標準庫的類前置聲明. 在std命名空間聲明實體是未定義的行為, 會導致如不可移植. 聲明標準庫下的實體, 需要包含對應的頭文件.
不應該使用using 指示引入整個命名空間的標識符號。
// 禁止 —— 污染命名空間using namespace foo;
不要在頭文件中使用命名空間別名除非顯式標記內部命名空間使用。因為任何在頭文件中引入的命名空間都會成為公開API的一部分。
// 在 .cc 中使用別名縮短常用的命名空間namespace baz = ::foo::bar::baz;// 在 .h 中使用別名縮短常用的命名空間namespace librarian {namespace impl { // 僅限內部使用namespace sidetable = ::pipeline_diagnostics::sidetable;} // namespace implinline void my_inline_function() { // 限制在一個函數中的命名空間別名 namespace baz = ::foo::bar::baz; ...}} // namespace librarian
禁止用內聯命名空間
2.2. 匿名命名空間和靜態變量
Tip
在.cc文件中定義一個不需要被外部引用的變量時,可以將它們放在匿名命名空間或聲明為static。但是不要在.h文件中這么做。
定義:
所有置于匿名命名空間的聲明都具有內部鏈接性,函數和變量可以經由聲明為static擁有內部鏈接性,這意味著你在這個文件中聲明的這些標識符都不能在另一個文件中被訪問。即使兩個文件聲明了完全一樣名字的標識符,它們所指向的實體實際上是完全不同的。
結論:
推薦、鼓勵在.cc中對于不需要在其他地方引用的標識符使用內部鏈接性聲明,但是不要在.h中使用。
匿名命名空間的聲明和具名的格式相同,在最后注釋上namespace:
namespace {...} // namespace
2.3. 非成員函數、靜態成員函數和全局函數
Tip
使用靜態成員函數或命名空間內的非成員函數, 盡量不要用裸的全局函數. 將一系列函數直接置于命名空間中,不要用類的靜態方法模擬出命名空間的效果,類的靜態方法應當和類的實例或靜態數據緊密相關.
優點:
某些情況下, 非成員函數和靜態成員函數是非常有用的, 將非成員函數放在命名空間內可避免污染全局作用域.
缺點:
將非成員函數和靜態成員函數作為新類的成員或許更有意義, 當它們需要訪問外部資源或具有重要的依賴關系時更是如此.
結論:
有時, 把函數的定義同類的實例脫鉤是有益的, 甚至是必要的. 這樣的函數可以被定義成靜態成員, 或是非成員函數. 非成員函數不應依賴于外部變量, 應盡量置于某個命名空間內. 相比單純為了封裝若干不共享任何靜態數據的靜態成員函數而創建類, 不如使用2.1. 命名空間。舉例而言,對于頭文件myproject/foo_bar.h, 應當使用
namespace myproject {namespace foo_bar {void Function1();void Function2();} // namespace foo_bar} // namespace myproject
而非
namespace myproject {class FooBar { public: static void Function1(); static void Function2();};} // namespace myproject
定義在同一編譯單元的函數, 被其他編譯單元直接調用可能會引入不必要的耦合和鏈接時依賴; 靜態成員函數對此尤其敏感. 可以考慮提取到新類中, 或者將函數置于獨立庫的命名空間內.
如果你必須定義非成員函數, 又只是在.cc文件中使用它, 可使用匿名2.1. 命名空間或static鏈接關鍵字 (如staticintFoo(){...}) 限定其作用域.
2.4. 局部變量
Tip
將函數變量盡可能置于最小作用域內, 并在變量聲明時進行初始化.
C++ 允許在函數的任何位置聲明變量. 我們提倡在盡可能小的作用域中聲明變量, 離第一次使用越近越好. 這使得代碼瀏覽者更容易定位變量聲明的位置, 了解變量的類型和初始值. 特別是,應使用初始化的方式替代聲明再賦值, 比如:
int i;i = f(); // 壞——初始化和聲明分離int j = g(); // 好——初始化時聲明vector
屬于if,while和for語句的變量應當在這些語句中正常地聲明,這樣子這些變量的作用域就被限制在這些語句中了,舉例而言:
while (const char* p = strchr(str, '/')) str = p + 1;
Warning
有一個例外, 如果變量是一個對象, 每次進入作用域都要調用其構造函數, 每次退出作用域都要調用其析構函數. 這會導致效率降低.
// 低效的實現for (int i = 0; i < 1000000; ++i) { Foo f; // 構造函數和析構函數分別調用 1000000 次! f.DoSomething(i);}
在循環作用域外面聲明這類變量要高效的多:
Foo f; // 構造函數和析構函數只調用 1 次for (int i = 0; i < 1000000; ++i) { f.DoSomething(i);}
2.5. 靜態和全局變量
Tip
禁止定義靜態儲存周期非POD變量,禁止使用含有副作用的函數初始化POD全局變量,因為多編譯單元中的靜態變量執行時的構造和析構順序是未明確的,這將導致代碼的不可移植。
禁止使用類的靜態儲存周期變量:由于構造和析構函數調用順序的不確定性,它們會導致難以發現的 bug 。不過constexpr變量除外,畢竟它們又不涉及動態初始化或析構。
靜態生存周期的對象,即包括了全局變量,靜態變量,靜態類成員變量和函數靜態變量,都必須是原生數據類型 (POD : Plain Old Data): 即 int, char 和 float, 以及 POD 類型的指針、數組和結構體。
靜態變量的構造函數、析構函數和初始化的順序在 C++ 中是只有部分明確的,甚至隨著構建變化而變化,導致難以發現的 bug. 所以除了禁用類類型的全局變量,我們也不允許用函數返回值來初始化 POD 變量,除非該函數(比如getenv()或getpid())不涉及任何全局變量。函數作用域里的靜態變量除外,畢竟它的初始化順序是有明確定義的,而且只會在指令執行到它的聲明那里才會發生。
Note
Xris 譯注:
同一個編譯單元內是明確的,靜態初始化優先于動態初始化,初始化順序按照聲明順序進行,銷毀則逆序。不同的編譯單元之間初始化和銷毀順序屬于未明確行為 (unspecified behaviour)。
同理,全局和靜態變量在程序中斷時會被析構,無論所謂中斷是從main()返回還是對exit()的調用。析構順序正好與構造函數調用的順序相反。但既然構造順序未定義,那么析構順序當然也就不定了。比如,在程序結束時某靜態變量已經被析構了,但代碼還在跑——比如其它線程——并試圖訪問它且失敗;再比如,一個靜態 string 變量也許會在一個引用了前者的其它變量析構之前被析構掉。
改善以上析構問題的辦法之一是用quick_exit()來代替exit()并中斷程序。它們的不同之處是前者不會執行任何析構,也不會執行atexit()所綁定的任何 handlers. 如果您想在執行quick_exit()來中斷時執行某 handler(比如刷新 log),您可以把它綁定到_at_quick_exit(). 如果您想在exit()和quick_exit()都用上該 handler, 都綁定上去。
綜上所述,我們只允許 POD 類型的靜態變量,即完全禁用vector(使用 C 數組替代) 和string(使用constchar[])。
如果您確實需要一個class類型的靜態或全局變量,可以考慮在main()函數或pthread_once()內初始化一個指針且永不回收。注意只能用 raw 指針,別用智能指針,畢竟后者的析構函數涉及到上文指出的不定順序問題。
Note
Yang.Y 譯注:
上文提及的靜態變量泛指靜態生存周期的對象, 包括: 全局變量, 靜態變量, 靜態類成員變量, 以及函數靜態變量.
譯者 (YuleFox) 筆記
cc中的匿名命名空間可避免命名沖突, 限定作用域, 避免直接使用using關鍵字污染命名空間;
嵌套類符合局部使用原則, 只是不能在其他頭文件中前置聲明, 盡量不要public;
盡量不用全局函數和全局變量, 考慮作用域和命名空間限制, 盡量單獨形成編譯單元;
多線程中的全局變量 (含靜態成員變量) 不要使用class類型 (含 STL 容器), 避免不明確行為導致的 bug.
作用域的使用, 除了考慮名稱污染, 可讀性之外, 主要是為降低耦合, 提高編譯/執行效率.
譯者(acgtyrant)筆記
注意「using 指示(using-directive)」和「using 聲明(using-declaration)」的區別。
匿名命名空間說白了就是文件作用域,就像 C static 聲明的作用域一樣,后者已經被 C++ 標準提倡棄用。
局部變量在聲明的同時進行顯式值初始化,比起隱式初始化再賦值的兩步過程要高效,同時也貫徹了計算機體系結構重要的概念「局部性(locality)」。
注意別在循環犯大量構造和析構的低級錯誤。
3. 類
類是 C++ 中代碼的基本單元. 顯然, 它們被廣泛使用. 本節列舉了在寫一個類時的主要注意事項.
3.1. 構造函數的職責
總述
不要在構造函數中調用虛函數, 也不要在無法報出錯誤時進行可能失敗的初始化.
定義
在構造函數中可以進行各種初始化操作.
優點
無需考慮類是否被初始化.
經過構造函數完全初始化后的對象可以為const類型, 也能更方便地被標準容器或算法使用.
缺點
如果在構造函數內調用了自身的虛函數, 這類調用是不會重定向到子類的虛函數實現. 即使當前沒有子類化實現, 將來仍是隱患.
在沒有使程序崩潰 (因為并不是一個始終合適的方法) 或者使用異常 (因為已經被禁用了) 等方法的條件下, 構造函數很難上報錯誤
如果執行失敗, 會得到一個初始化失敗的對象, 這個對象有可能進入不正常的狀態, 必須使用boolisValid()或類似這樣的機制才能檢查出來, 然而這是一個十分容易被疏忽的方法.
構造函數的地址是無法被取得的, 因此, 舉例來說, 由構造函數完成的工作是無法以簡單的方式交給其他線程的.
結論
構造函數不允許調用虛函數. 如果代碼允許, 直接終止程序是一個合適的處理錯誤的方式. 否則, 考慮用Init()方法或工廠函數.
構造函數不得調用虛函數, 或嘗試報告一個非致命錯誤. 如果對象需要進行有意義的 (non-trivial) 初始化, 考慮使用明確的 Init() 方法或使用工廠模式. AvoidInit()methods on objects with no other states that affect which public methods may be called (此類形式的半構造對象有時無法正確工作).
3.2. 隱式類型轉換
總述
不要定義隱式類型轉換. 對于轉換運算符和單參數構造函數, 請使用explicit關鍵字.
定義
隱式類型轉換允許一個某種類型 (稱作源類型) 的對象被用于需要另一種類型 (稱作目的類型) 的位置, 例如, 將一個int類型的參數傳遞給需要double類型的函數.
除了語言所定義的隱式類型轉換, 用戶還可以通過在類定義中添加合適的成員定義自己需要的轉換. 在源類型中定義隱式類型轉換, 可以通過目的類型名的類型轉換運算符實現 (例如operatorbool()). 在目的類型中定義隱式類型轉換, 則通過以源類型作為其唯一參數 (或唯一無默認值的參數) 的構造函數實現.
explicit關鍵字可以用于構造函數或 (在 C++11 引入) 類型轉換運算符, 以保證只有當目的類型在調用點被顯式寫明時才能進行類型轉換, 例如使用cast. 這不僅作用于隱式類型轉換, 還能作用于 C++11 的列表初始化語法:
class Foo { explicit Foo(int x, double y); ...};void Func(Foo f);
此時下面的代碼是不允許的:
Func({42, 3.14}); // Error
這一代碼從技術上說并非隱式類型轉換, 但是語言標準認為這是explicit應當限制的行為.
優點
有時目的類型名是一目了然的, 通過避免顯式地寫出類型名, 隱式類型轉換可以讓一個類型的可用性和表達性更強.
隱式類型轉換可以簡單地取代函數重載.
在初始化對象時, 列表初始化語法是一種簡潔明了的寫法.
缺點
隱式類型轉換會隱藏類型不匹配的錯誤. 有時, 目的類型并不符合用戶的期望, 甚至用戶根本沒有意識到發生了類型轉換.
隱式類型轉換會讓代碼難以閱讀, 尤其是在有函數重載的時候, 因為這時很難判斷到底是哪個函數被調用.
單參數構造函數有可能會被無意地用作隱式類型轉換.
如果單參數構造函數沒有加上explicit關鍵字, 讀者無法判斷這一函數究竟是要作為隱式類型轉換, 還是作者忘了加上explicit標記.
并沒有明確的方法用來判斷哪個類應該提供類型轉換, 這會使得代碼變得含糊不清.
如果目的類型是隱式指定的, 那么列表初始化會出現和隱式類型轉換一樣的問題, 尤其是在列表中只有一個元素的時候.
結論
在類型定義中, 類型轉換運算符和單參數構造函數都應當用explicit進行標記. 一個例外是, 拷貝和移動構造函數不應當被標記為explicit, 因為它們并不執行類型轉換. 對于設計目的就是用于對其他類型進行透明包裝的類來說, 隱式類型轉換有時是必要且合適的. 這時應當聯系項目組長并說明特殊情況.
不能以一個參數進行調用的構造函數不應當加上explicit. 接受一個std::initializer_list作為參數的構造函數也應當省略explicit, 以便支持拷貝初始化 (例如MyTypem={1,2};).
3.3. 可拷貝類型和可移動類型
總述
如果你的類型需要, 就讓它們支持拷貝 / 移動. 否則, 就把隱式產生的拷貝和移動函數禁用.
定義
可拷貝類型允許對象在初始化時得到來自相同類型的另一對象的值, 或在賦值時被賦予相同類型的另一對象的值, 同時不改變源對象的值. 對于用戶定義的類型, 拷貝操作一般通過拷貝構造函數與拷貝賦值操作符定義.string類型就是一個可拷貝類型的例子.
可移動類型允許對象在初始化時得到來自相同類型的臨時對象的值, 或在賦值時被賦予相同類型的臨時對象的值 (因此所有可拷貝對象也是可移動的).std::unique_ptr
拷貝 / 移動構造函數在某些情況下會被編譯器隱式調用. 例如, 通過傳值的方式傳遞對象.
優點
可移動及可拷貝類型的對象可以通過傳值的方式進行傳遞或者返回, 這使得 API 更簡單, 更安全也更通用. 與傳指針和引用不同, 這樣的傳遞不會造成所有權, 生命周期, 可變性等方面的混亂, 也就沒必要在協議中予以明確. 這同時也防止了客戶端與實現在非作用域內的交互, 使得它們更容易被理解與維護. 這樣的對象可以和需要傳值操作的通用 API 一起使用, 例如大多數容器.
拷貝 / 移動構造函數與賦值操作一般來說要比它們的各種替代方案, 比如Clone(),CopyFrom()orSwap(), 更容易定義, 因為它們能通過編譯器產生, 無論是隱式的還是通過=default. 這種方式很簡潔, 也保證所有數據成員都會被復制. 拷貝與移動構造函數一般也更高效, 因為它們不需要堆的分配或者是單獨的初始化和賦值步驟, 同時, 對于類似省略不必要的拷貝這樣的優化它們也更加合適.
移動操作允許隱式且高效地將源數據轉移出右值對象. 這有時能讓代碼風格更加清晰.
缺點
許多類型都不需要拷貝, 為它們提供拷貝操作會讓人迷惑, 也顯得荒謬而不合理. 單件類型 (Registerer), 與特定的作用域相關的類型 (Cleanup), 與其他對象實體緊耦合的類型 (Mutex) 從邏輯上來說都不應該提供拷貝操作. 為基類提供拷貝 / 賦值操作是有害的, 因為在使用它們時會造成對象切割. 默認的或者隨意的拷貝操作實現可能是不正確的, 這往往導致令人困惑并且難以診斷出的錯誤.
拷貝構造函數是隱式調用的, 也就是說, 這些調用很容易被忽略. 這會讓人迷惑, 尤其是對那些所用的語言約定或強制要求傳引用的程序員來說更是如此. 同時, 這從一定程度上說會鼓勵過度拷貝, 從而導致性能上的問題.
結論
如果需要就讓你的類型可拷貝 / 可移動. 作為一個經驗法則, 如果對于你的用戶來說這個拷貝操作不是一眼就能看出來的, 那就不要把類型設置為可拷貝. 如果讓類型可拷貝, 一定要同時給出拷貝構造函數和賦值操作的定義, 反之亦然. 如果讓類型可拷貝, 同時移動操作的效率高于拷貝操作, 那么就把移動的兩個操作 (移動構造函數和賦值操作) 也給出定義. 如果類型不可拷貝, 但是移動操作的正確性對用戶顯然可見, 那么把這個類型設置為只可移動并定義移動的兩個操作.
如果定義了拷貝/移動操作, 則要保證這些操作的默認實現是正確的. 記得時刻檢查默認操作的正確性, 并且在文檔中說明類是可拷貝的且/或可移動的.
class Foo { public: Foo(Foo&& other) : field_(other.field) {} // 差, 只定義了移動構造函數, 而沒有定義對應的賦值運算符. private: Field field_;};
由于存在對象切割的風險, 不要為任何有可能有派生類的對象提供賦值操作或者拷貝 / 移動構造函數 (當然也不要繼承有這樣的成員函數的類). 如果你的基類需要可復制屬性, 請提供一個publicvirtualClone()和一個protected的拷貝構造函數以供派生類實現.
如果你的類不需要拷貝 / 移動操作, 請顯式地通過在public域中使用=delete或其他手段禁用之.
// MyClass is neither copyable nor movable.MyClass(const MyClass&) = delete;MyClass& operator=(const MyClass&) = delete;
3.4. 結構體 VS. 類
總述
僅當只有數據成員時使用struct, 其它一概使用class.
說明
在 C++ 中struct和class關鍵字幾乎含義一樣. 我們為這兩個關鍵字添加我們自己的語義理解, 以便為定義的數據類型選擇合適的關鍵字.
struct用來定義包含數據的被動式對象, 也可以包含相關的常量, 但除了存取數據成員之外, 沒有別的函數功能. 并且存取功能是通過直接訪問位域, 而非函數調用. 除了構造函數, 析構函數,Initialize(),Reset(),Validate()等類似的用于設定數據成員的函數外, 不能提供其它功能的函數.
如果需要更多的函數功能,class更適合. 如果拿不準, 就用class.
為了和 STL 保持一致, 對于仿函數等特性可以不用class而是使用struct.
注意: 類和結構體的成員變量使用不同的命名規則.
3.5. 繼承
總述
使用組合 (YuleFox 注: 這一點也是 GoF 在 <
定義
當子類繼承基類時, 子類包含了父基類所有數據及操作的定義. C++ 實踐中, 繼承主要用于兩種場合: 實現繼承, 子類繼承父類的實現代碼;接口繼承, 子類僅繼承父類的方法名稱.
優點
實現繼承通過原封不動的復用基類代碼減少了代碼量. 由于繼承是在編譯時聲明, 程序員和編譯器都可以理解相應操作并發現錯誤. 從編程角度而言, 接口繼承是用來強制類輸出特定的 API. 在類沒有實現 API 中某個必須的方法時, 編譯器同樣會發現并報告錯誤.
缺點
對于實現繼承, 由于子類的實現代碼散布在父類和子類間之間, 要理解其實現變得更加困難. 子類不能重寫父類的非虛函數, 當然也就不能修改其實現. 基類也可能定義了一些數據成員, 因此還必須區分基類的實際布局.
結論
所有繼承必須是public的. 如果你想使用私有繼承, 你應該替換成把基類的實例作為成員對象的方式.
不要過度使用實現繼承. 組合常常更合適一些. 盡量做到只在 “是一個” (“is-a”, YuleFox 注: 其他 “has-a” 情況下請使用組合) 的情況下使用繼承: 如果Bar的確 “是一種”Foo,Bar才能繼承Foo.
必要的話, 析構函數聲明為virtual. 如果你的類有虛函數, 則析構函數也應該為虛函數.
對于可能被子類訪問的成員函數, 不要過度使用protected關鍵字. 注意, 數據成員都必須是私有的.
對于重載的虛函數或虛析構函數, 使用override, 或 (較不常用的)final關鍵字顯式地進行標記. 較早 (早于 C++11) 的代碼可能會使用virtual關鍵字作為不得已的選項. 因此, 在聲明重載時, 請使用override,final或virtual的其中之一進行標記. 標記為override或final的析構函數如果不是對基類虛函數的重載的話, 編譯會報錯, 這有助于捕獲常見的錯誤. 這些標記起到了文檔的作用, 因為如果省略這些關鍵字, 代碼閱讀者不得不檢查所有父類, 以判斷該函數是否是虛函數.
3.6. 多重繼承
總述
真正需要用到多重實現繼承的情況少之又少. 只在以下情況我們才允許多重繼承: 最多只有一個基類是非抽象類; 其它基類都是以Interface為后綴的純接口類.
定義
多重繼承允許子類擁有多個基類. 要將作為純接口的基類和具有實現的基類區別開來.
優點
相比單繼承 (見繼承), 多重實現繼承可以復用更多的代碼.
缺點
真正需要用到多重實現繼承的情況少之又少. 有時多重實現繼承看上去是不錯的解決方案, 但這時你通常也可以找到一個更明確, 更清晰的不同解決方案.
結論
只有當所有父類除第一個外都是純接口類時, 才允許使用多重繼承. 為確保它們是純接口, 這些類必須以Interface為后綴.
注意
關于該規則, Windows 下有個特例.
3.7. 接口
總述
接口是指滿足特定條件的類, 這些類以Interface為后綴 (不強制).
定義
當一個類滿足以下要求時, 稱之為純接口:
只有純虛函數 (“=0”) 和靜態函數 (除了下文提到的析構函數).
沒有非靜態數據成員.
沒有定義任何構造函數. 如果有, 也不能帶有參數, 并且必須為protected.
如果它是一個子類, 也只能從滿足上述條件并以Interface為后綴的類繼承.
接口類不能被直接實例化, 因為它聲明了純虛函數. 為確保接口類的所有實現可被正確銷毀, 必須為之聲明虛析構函數 (作為上述第 1 條規則的特例, 析構函數不能是純虛函數). 具體細節可參考 Stroustrup 的The C++ Programming Language, 3rd edition第 12.4 節.
優點
以Interface為后綴可以提醒其他人不要為該接口類增加函數實現或非靜態數據成員. 這一點對于多重繼承尤其重要. 另外, 對于 Java 程序員來說, 接口的概念已是深入人心.
缺點
Interface后綴增加了類名長度, 為閱讀和理解帶來不便. 同時, 接口屬性作為實現細節不應暴露給用戶.
結論
只有在滿足上述條件時, 類才以Interface結尾, 但反過來, 滿足上述需要的類未必一定以Interface結尾.
3.8. 運算符重載
總述
除少數特定環境外, 不要重載運算符. 也不要創建用戶定義字面量.
定義
C++ 允許用戶通過使用operator關鍵字對內建運算符進行重載定義, 只要其中一個參數是用戶定義的類型.operator關鍵字還允許用戶使用operator""定義新的字面運算符, 并且定義類型轉換函數, 例如operatorbool().
優點
重載運算符可以讓代碼更簡潔易懂, 也使得用戶定義的類型和內建類型擁有相似的行為. 重載運算符對于某些運算來說是符合符合語言習慣的名稱 (例如==,<,?=,?<<), 遵循這些語言約定可以讓用戶定義的類型更易讀, 也能更好地和需要這些重載運算符的函數庫進行交互操作.
對于創建用戶定義的類型的對象來說, 用戶定義字面量是一種非常簡潔的標記.
缺點
要提供正確, 一致, 不出現異常行為的操作符運算需要花費不少精力, 而且如果達不到這些要求的話, 會導致令人迷惑的 Bug.
過度使用運算符會帶來難以理解的代碼, 尤其是在重載的操作符的語義與通常的約定不符合時.
函數重載有多少弊端, 運算符重載就至少有多少.
運算符重載會混淆視聽, 讓你誤以為一些耗時的操作和操作內建類型一樣輕巧.
對重載運算符的調用點的查找需要的可就不僅僅是像 grep 那樣的程序了, 這時需要能夠理解 C++ 語法的搜索工具.
如果重載運算符的參數寫錯, 此時得到的可能是一個完全不同的重載而非編譯錯誤. 例如:foo
重載某些運算符本身就是有害的. 例如, 重載一元運算符&會導致同樣的代碼有完全不同的含義, 這取決于重載的聲明對某段代碼而言是否是可見的. 重載諸如&&,||和,會導致運算順序和內建運算的順序不一致.
運算符從通常定義在類的外部, 所以對于同一運算, 可能出現不同的文件引入了不同的定義的風險. 如果兩種定義都鏈接到同一二進制文件, 就會導致未定義的行為, 有可能表現為難以發現的運行時錯誤.
用戶定義字面量所創建的語義形式對于某些有經驗的 C++ 程序員來說都是很陌生的.
結論
只有在意義明顯, 不會出現奇怪的行為并且與對應的內建運算符的行為一致時才定義重載運算符. 例如,|要作為位或或邏輯或來使用, 而不是作為 shell 中的管道.
只有對用戶自己定義的類型重載運算符. 更準確地說, 將它們和它們所操作的類型定義在同一個頭文件中,.cc中和命名空間中. 這樣做無論類型在哪里都能夠使用定義的運算符, 并且最大程度上避免了多重定義的風險. 如果可能的話, 請避免將運算符定義為模板, 因為此時它們必須對任何模板參數都能夠作用. 如果你定義了一個運算符, 請將其相關且有意義的運算符都進行定義, 并且保證這些定義的語義是一致的. 例如, 如果你重載了<, 那么請將所有的比較運算符都進行重載, 并且保證對于同一組參數,?不會同時返回true.
建議不要將不進行修改的二元運算符定義為成員函數. 如果一個二元運算符被定義為類成員, 這時隱式轉換會作用域右側的參數卻不會作用于左側. 這時會出現a
不要為了避免重載操作符而走極端. 比如說, 應當定義==,=, 和<
不要重載&&,||,,或一元運算符&. 不要重載operator"", 也就是說, 不要引入用戶定義字面量.
類型轉換運算符在隱式類型轉換一節有提及.=運算符在可拷貝類型和可移動類型一節有提及. 運算符<
3.9. 存取控制
總述
將所有數據成員聲明為private, 除非是staticconst類型成員 (遵循常量命名規則). 處于技術上的原因, 在使用Google Test時我們允許測試固件類中的數據成員為protected.
3.10. 聲明順序
總述
將相似的聲明放在一起, 將public部分放在最前.
說明
類定義一般應以public:開始, 后跟protected:, 最后是private:. 省略空部分.
在各個部分中, 建議將類似的聲明放在一起, 并且建議以如下的順序: 類型 (包括typedef,using和嵌套的結構體與類), 常量, 工廠函數, 構造函數, 賦值運算符, 析構函數, 其它函數, 數據成員.
不要將大段的函數定義內聯在類定義中. 通常,只有那些普通的, 或性能關鍵且短小的函數可以內聯在類定義中. 參見內聯函數一節.
譯者 (YuleFox) 筆記
不在構造函數中做太多邏輯相關的初始化;
編譯器提供的默認構造函數不會對變量進行初始化, 如果定義了其他構造函數, 編譯器不再提供, 需要編碼者自行提供默認構造函數;
為避免隱式轉換, 需將單參數構造函數聲明為explicit;
為避免拷貝構造函數, 賦值操作的濫用和編譯器自動生成, 可將其聲明為private且無需實現;
僅在作為數據集合時使用struct;
組合 > 實現繼承 > 接口繼承 > 私有繼承, 子類重載的虛函數也要聲明virtual關鍵字, 雖然編譯器允許不這樣做;
避免使用多重繼承, 使用時, 除一個基類含有實現外, 其他基類均為純接口;
接口類類名以Interface為后綴, 除提供帶實現的虛析構函數, 靜態成員函數外, 其他均為純虛函數, 不定義非靜態數據成員, 不提供構造函數, 提供的話, 聲明為protected;
為降低復雜性, 盡量不重載操作符, 模板, 標準類中使用時提供文檔說明;
存取函數一般內聯在頭文件中;
聲明次序:public->protected->private;
函數體盡量短小, 緊湊, 功能單一;
-
Google
+關注
關注
5文章
1762瀏覽量
57506 -
編程
+關注
關注
88文章
3614瀏覽量
93686 -
C++
+關注
關注
22文章
2108瀏覽量
73622
原文標題:Google C++ 編程規范 - 2
文章出處:【微信號:C_Expert,微信公眾號:C語言專家集中營】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論