北京時間2016年1月9日10:31:06,正式開始翻譯。水平有限,各位看官若有覺得不妥之處,請批評指正。
現在就開始《Effective Modern C++》翻譯之旅,第一個姿勢--簡介
Introduction
如果您是一位專家級別的C++工程師,和我最初接觸C++11的時候想法一樣:“是的,我明白了,C++11只不過是比C++多了一點東西而已”。但是隨著您對這個修訂後語言的理解加深,你便會被它的變化之大感到震驚。自動類型推斷、基於范圍的for循環、lambda表達式、右值引用等改變了C++的面貌,更不要說新增加的並發特性。還有那些符合語言特性習慣的變化!0和typedef關鍵字被淘汰,取而代之的是nullptr表示空指針和別名聲明(alias declaration)。枚舉有了作用域,智能指針成為了更加完美的內置類型。移動對象往往比拷貝對象更加靠譜。所以,C++11有很多值得學習的東西。
C++14的通過並沒有讓這些變得簡單。
所以,需要學習的地方很多。更重要的是,如何高效的使用這些新特性。如果您需要C++11的基本的語法和語義,資源總量比較豐富,但如果你正在尋找如何指導您編寫正確的,有效的,可維護代碼的資料時,這樣的搜索非常具挑戰性。這就是這本書的原因。這裡不是專門描述的C ++ 11和C ++14的特性,而是告訴你如何有效的應用C++11。
這本書裡的信息分為一條一條的,我們稱之為條款。想要理解變量的類型推斷?或者想要明確什麼時候應該(或者什麼時候不應該)使用auto來聲明變量?您對為什麼const成員函數應該是線程安全的感興趣?如何使用std::unique_ptr實現pimpl?為什麼你在使用lambda表達式時應該避免默認的變量捕捉形式?或者是std::atomic和volatile的區別(如何正確的使用它們)?這些問題的答案你都可以在書中找到,除此之外,這些答案都是平台獨立,與標准一致的,這是一本關於C++的可移植的書。
每一個條款都是一個指導方針,而不是准則,這是因為指導方針是有例外的。每一個條款中最主要的部分不在於它提出的建議,而是這些建議背後的原理和思考的過程。一旦你讀完了這本書,將來是由你來決定在你的項目的環境中,是否應該忽視或者應用這些條款中的指導。這本書的真正目的不在於告訴你應該做什麼、避免做什麼,而是傳遞對C++11和C++14如何工作的更深層次的理解。
術語和習慣
為了確保我們彼此理解,在一些術語上達成一致非常重要。C++有4個標准,命名規則是被ISO標准采用的年份,C++98,C++03,C++11和C++14。C++98和C++03只是存在一些微妙的技術細節上的差別,所以在這本書裡,我把二者都稱為C++98。
當我提到C++98的時候,我指的只是C++語言的這個版本。在我提到C++11的地方,我指的是C++11和C++14,因為C++14是C++11的一個有效的超集。當我寫C++14的時候,我明確的指的是C++14,如果我只是簡單的提到C++,那麼它是屬於所有語言版本的。所以,我可能會說C++是十分重視效率的(這裡指的是所有的C++版本), C++98缺少對並發性的支持(指的僅僅是C++98), C++11支持了lambda表達式(指的C++11和C++14),C++14提供了更普遍的函數返回類型的推導(指的僅僅是C++14)。
C++11最流行的特性很可能是移動語義,移動語義的基礎是區分表達式中的是左值還是右值。因為右值暗示了對象有資格使用移動運算,而左值通常不能。在概念上(盡管並不總是在實踐中)右值相對應於從函數返回的匿名的臨時變量,而左值相對應於你可以引用的對象,既可以通過指針,也可以通過引用。
來判斷一個表達式是不是左值的有效方法是看你能不能取它的地址,如果能的話,它通常就是一個左值。如果你不能的話,它通常是一個右值。這個方法的一個好的特性在於它幫助你記住了一個表達式的類型和這個表達式代表的是一個左值還是一個右值是無關的,給一個類型T,你既可以獲得T的左值類型,也可以獲得T的右值類型,這是十分重要的,尤其是當你處理一個右值的引用參數的時候,因為這個時候參數本身是一個左值。
class Widget {
public:
Widget(Widget&& rhs); // rhs is an lvalue, though it has
.... //an rvalue reference type
};
這裡,在widgt的移動構造函數中取得rhs參數的地址是完全合法的,所以rhs是一個左值,盡管它的類型是一個右值的引用(類似的推理,一切參數都是左值)。
這段代碼展示了很多我通常遵循的約定:
?類的名字是widget,當我想要表示一個任意的用戶自定義類型的時候使用widget。我會不加聲明的使用widget,除了某些時候,我需要展示類的特殊的細節。
?我把參數命名為rhs,代表了right-hand side,這是我在使用移動操作(比如移動構造,移動賦值)和拷貝操作(比如拷貝構造,拷貝賦值)時比較偏愛的名字,盡管我在使用二元運算符也通常使用rhs作為右面參數的名字。
Matrix operator+(const Matrix& lhs,const Matrix& rhs);
我希望這不會令你感到驚訝,lhs代表了left-hand side。
?我高亮了代碼或者注釋的部分內容,來使你的注意力集中到上面去,在上面的代碼中,我加亮了rhs和注釋的部分內容,使你注意到rhs是一個左值。
?我使用“…”來暗示這裡會有其他的代碼,這裡窄的省略號和寬的省略號(“…”)間是有區別的,寬的省略號是在C++11中作為變長模板使用的,這聽起來有點令人困惑,其實不是,例如
template // these are C++
void processVals(const Ts&... params) // source code
{ // ellipses 28
… // this means "some
// code goes here"
}
processVals聲明顯示了我在聲明模板參數的時候使用了typename,這只是個人的偏愛。class在這裡同樣適用,僅僅在我展示一些來自C++標准中的代碼引用的時候,我會使用class聲明模板的參數,因為標准裡就是這樣做的。
當一個對象以另一個同樣類型的對象初始化的時候,這個新的對象被認為原對象的一個拷貝,即使這個拷貝是經由移動構造創建的,令人遺憾的是,C++中沒有任何一個技術可以區分一個對象是經由拷貝構造創建的,還是經由移動構造創建的。
void someFunc(Widget w); // someFunc's parameter w
// is passed by value
Widget wid; // wid is some Widget
someFunc(wid); // in this call to someFunc,
// w is a copy of wid that's
// created via copy construction
someFunc(std::move(wid)); // in this call to SomeFunc,
// w is a copy of wid that's
// created via move construction
右值的拷貝通常是通過移動構造的,左值的拷貝通常是通過拷貝構造的。這裡暗示了我們,如果你僅僅知道一個對象是另一個對象的一個拷貝,你無法知道構造這個拷貝的花費。比如在上面的代碼中,當你不知道是一個左值還是一個右值被傳遞給someFunc的參數w的時,你無法知道創建參數w所需要的花費(你同樣需要知道拷貝構造和一個構造widget的花費)。
在一個函數調用中,調用端的表達式是這個函數的實參(argument),這些參數被用來實例化函數的形參(parameters)。在第一個例子中,實參是wid,在第二個例子中,實參是std::move(wid)。在這兩個例子中, 形參都是w,形參和實參的區別是很重要的。因為形參是左值,但是實參和實例化這些實參的卻可能是左值或是右值,這個和完美轉發(perfect forwarding)的過程相關。完美轉發是指將參數傳遞給函數中調用的第二個函數,原來參數的左值和右值性得以保留(將在條款32中進行討論完美轉發的更多細節)。
精心設計的函數是異常安全的(exception-safe),這意味著他們至少提供了最基本的異常安全保證(即基本承諾basic guarantee)。這樣的函數向調用者確保了即使有一個異常產生了,程序的不變量依舊是完整的(即沒有任何數據結構被破壞),也沒有任何資源的洩露,那些提供了強烈的異常安全保證(即強烈保證strong guarantee)的函數,向調用者確保了如果有一個異常產生了,程序的狀態和調用前是一樣的。就像條款16解釋的那樣,C++98標准類庫裡的函數提供了強烈保證約束對於C++11標准類庫裡的移動語義(C++98 Standard Library functions offering the strong guarantee constrain the applicability of move semantics in the C++11 Standard Library.)。
我使用術語可調用實體(callable entity)來描述可以和調用非成員函數一樣的調用語法的任何東西,比如,語法“functionName(arguments)“,函數,函數指針,函數對象都是可調用實體(callable entity)。通過lambda表達式創建的函數對象被稱為閉包(closures),很少有必要去區分一個lambda表達式和它們創建的閉包,所以我把它們都稱作lambdas。
同樣的,我幾乎不區分函數模板(即產生函數的模板)和模板函數(即從模板裡實例化的函數),類模板和模板類也一樣。
C++裡的很多東西可以被聲明和定義,聲明給出了它的名字,卻沒有給出太多的細節,比如它的儲存空間和它是如何實現的。
extern int x; // object declaration
class Widget; // class declaration
int func(const Widget& w); // function declaration
enum class Color; // scoped enum declaration
// (see Item 10)
定義提供了它的儲存空間和它的實現細節。
class Widget { // class definition
…
};
int func(const Widget& w)
{ return w.size(); } // function definition
enum class Color
{ Yellow, Red, Blue }; // scoped enum definition
定義同樣包括聲明,所以除非某些東西當它作為定義很重要時,一般情況下,我傾向於使用聲明。
新的C++標准保留了原有的在舊的標准下寫的代碼的有效性,但是標准委員會偶爾也會棄用(deprecates)一些特性。這警告一個特性可能會在未來的標准中被移除,你應該避免使用這些被否決的特性(被否決的原因通常是新的特性提供了一樣的功能,但是帶有更少的限制和缺點),例如std::auto_ptr在C++11中被否決,因為std::unique_ptr提供了同樣的功能,而且做的更好。
有時,標准會說一個操作的結果是未定義的(undefined behavior),這意味著運行時的行為是無法預測的,毫無疑問,你想要避開這樣的不確定性,未定義的行為有使用中括號([])時下標超過了std::vector的界限,解引用一個未實例化的迭代器,或者涉及到數據競爭(例如有兩個以上的線程,至少一個是寫者,同時訪問一個內存單元)。
在源代碼的注釋中,有時我把construct縮寫為ctor,把destructor縮寫為dtor。
報告Bugs和改進建議
我盡我最大努力讓這本書充滿了清晰,具體,有用的信息,但是肯定還有一些方式使它變的更好。如何你發現了任何形式的錯誤(技術的,解釋說明的,語法的,排版的等等),或者你有關於改進這本書的建議,請發郵件到我的郵箱 [email protected] 。新的印刷給我機會來修訂Effective Modern C++,但我無法解決我不知道的問題。
譯者注釋:
Pimpl Idiom 它可以用來降低文件間的編譯依賴關系