C++11新特性

C++11標準發佈已有一段時間了, 維基百科上有對C++11新標準的變化和C++11新特性介紹的文章. 我是一名C++程序員,非常想了解一下C++11. 英文版的維基百科看起來非常費勁,而中文版維基百科不是知道是臺灣還是香港人翻譯的然後由工具轉換成簡體中文的,有些術語和語言習慣和大陸程序不一樣! 我決定結合這兩個版本按照我自己的習慣把這篇文章整理到我的博客中.分享給關注我和關注C++11的朋友們. 當然了, 本人水平有限,英語水平也很一般,就把這個過程當做學習C++11的過程吧.文章中肯定會有很多錯誤或描述不恰當的地方. 非常希望看到的朋友能給我指出來.

以下是關於C++11的英文版本和中文版本維基百科的鏈接:

http://en.wikipedia.org/wiki/C++11

http://zh.wikipedia.org/wiki/C++0x

   目錄

   0.簡介

C++11,之前被稱作C++0x,即ISO/IEC 14882:2011,是目前的C++編程語言的正式標準。它取代第二版標準ISO/IEC 14882:2003(第一版ISO/IEC 14882:1998發佈於1998年,第二版於2003年發佈,分別通稱C++98以及C++03,兩者差異很小)。新的標準包含了幾個核心語言增加的新特性,而且擴展C++標準程序庫,併入了大部分的C++ Technical Report 1程序庫(數學的特殊函數除外)。最新的消息被公佈在 ISO C++ 委員會網站(英文)。

ISO/IEC JTC1/SC22/WG21 C++ 標準委員會計劃在2010年8月之前完成對最終委員會草案的投票,以及於2011年3月召開的標準會議完成國際標準的最終草案。然而,WG21預期ISO將要花費六個月到一年的時間才能正式發佈新的C++標準。爲了能夠如期完成,委員會決定致力於直至2006年爲止的提案,忽略新的提案。最終,於2011年8月12日公佈,並於2011年9月出版。2012年2月28日的國際標準草案(N3376)是最接近於現行標準的草案,差異僅有編輯上的修正。

像C++這樣的編程語言,通過一種演化的過程來發展其定義。這個過程不可避免地將引發與現有代碼的兼容問題。不過根據Bjarne Stroustrup(C++的創始人,標準委員會的一員)表示,新的標準將幾乎100%兼容現有標準。

 

1.來自以往版本標準的變更(候選變更)

C++的修訂範圍包括核心語言以及標準程序庫。在開發2011版標準的各個特性的過程中,標準委員會堅持以下指導思想:
    * 維持與C++98,可能的話還有C之間的兼容性穩定性
    * 儘可能通過通過標準程序庫來引進新的特性, 而不是擴展核心語言;
    * 能夠促進編程技術的變更優先;
    * 改進 C++ 以幫助系統和程序庫的設計,而不是引進只對特定應用有用的新特性;
    * 增強類型安全,給現行不安全的技術提供更安全的替代方案;
    * 增強直接與硬件協同工作的性能和能力;
    * 爲現實世界中的問題提供適當的解決方案;
    * 實行零負擔原則(如果某些功能要求的額外支持,那麼只有在該功能被用到時這些額外的支持才被用到);
    * 使C++易於教學

注重對初學者的關注,因爲他們構成了計算機程序員的主體。也因爲許多初學者不願擴展他們的C++知識,他們僅限於掌握C++中自己所專精的部分. 

 

2.核心語言的擴充

C++標準委員會的一個職責是開發語言核心. 核心語言被大幅改進的領域包括: 多線程支持, 泛型編程支持, 統一的初始化和提高性能.
    這篇文章將核心語言的特性和變化大致分爲: 提高運行期性能, 提高編譯期性能, 增強可用性, 和新特性4大類. 某些特性可以被劃分到多個分類中, 但只會在主要體現該特性的分類中討論一次.

 

3.提高核心語言運行性能
    以下特性主要是爲了提高性能提或計算速度等設計的.

3.1 右值引用和移動構造
    在C++03及之前的標準中,臨時對象(稱爲右值"R-values",因爲通常位於賦值運算符的右邊)的值是不能改變的,和C語言一樣, 且無法和 const T& 類型做出區分。儘管在某些情況下臨時對象的確會被改變,甚至有時還被視爲是一個有用的漏洞。C++11新增加了一個非常量引用(non-const reference)類型,稱作右值引用(R-value reference),標記爲T &&。右值引用所引用的臨時對象可以在該臨時對象被初始化之後做修改,這是爲了允許 move 語義。
    C++03 性能上被長期詬病的問題之一,就是其耗時且不必要的深拷貝。深拷貝會隱式地發生在以傳值的方式傳遞對象的時候。例如,std::vector內部封裝了一個C風格的數組和其元素個數,如果創建或是從函數返回一個std::vector的臨時對象,要將其保存起來只能通過生成新的std::vector對象並且把該臨時對象所有的數據複製進去。該臨時對象和其擁有的內存會被銷燬。(爲簡單起見,這裏忽略了編譯器的返回值優化)
    在 C++11中,std::vector有一個"移動構造函數",對某個vector的右值引用可以單純地從右值複製其內部C風格數組的指針到新的vector中,然後將右值中的指針置空。因爲這個臨時對象不會再被使用,沒代碼會再訪問這個空指針, 而且因爲這個臨時對象的內部指針是NULL,所以當這個臨時對象離開作用域時它的內存也不會被釋放掉.所以,這個操作不僅沒有代價高昂的深拷貝, 還是安全的,對用戶不可見的!

這個操作不需要數組的複製,而且空的臨時對象的析構也不會銷燬內存。返回vector臨時對象的函數只需要返回std::vector<T>&&。如果vector沒有move 構造函數,那麼就會調用常規拷貝構造函數。如果有,那麼就會優先調用move構造函數,這能夠避免大量的內存分配和內存拷貝操作。

右值引用不用對標準庫之外的代碼做任何改動就可以爲已有代碼帶來性能上的提升. 返回值類型爲std::vector<T>的函數返回了一個std::vector<T>類型的臨時對象,爲了使用移動構造不需要顯示地將返回值類型改爲std::vector<T>&&, 因爲這樣的臨時對象會被自動當作右值引用. 但是在c++03中, std::vector<T>沒有移動構造函數, 帶有const std::vector<T>& 參數的拷貝構造會被調用, 這會導致大量內存分配和拷貝動作.
出於安全考慮, 需要施加一些限制! 一個已命名的變量即使聲明爲右值,也不會被視爲右值.想要獲取一個右值,應該使用模板函數std::move<T>(). 右值引用也可以在特定情況下被修改, 主要是爲了與移動構造函數一起使用!
由於"右值引用"這個詞的自然語義,以及對"左值引用"(常規引用)這個詞的修正, 右值引用可以讓開發者提供完美的函數轉發! 與可變參數模板結合時, 這個能力讓模板函數能夠完美地將參數轉發給帶有這些參數的另一個函數.這對構造函數的參數轉發最爲有用,創建一個能夠根據特定的參數自動調用適當的構造函數的工廠函數.

3.2 泛化的常數表達式

  C++一直以來都有常量表達式的概念.這種表達式就像3+4這種在編譯期和運行時都能得到相同結果的表達式. 常量表達式給編譯器提供了優化的機會, 編譯器計算出他們的值並把結果硬編碼到程序中. 並且C++規格文檔中有很多地方要求使用常量表達式. 例如,定義一個數組需要常量表達式(來指定數組大小), 枚舉值必須是常量表達式.
      然而,常量表達式中從來都不允許調用函數或創建對象. 所以,像下面這樣的簡單代碼卻是非法的:

int get_five() {return 5;} 
int some_value[get_five() + 7]; // 創建一個包含12個整數的數組. 這種形式在C++中是非法的.

  這在C++03中是非法的, 因爲get_five() + 7不是常量表達式. C++03的編譯器在編譯期沒辦法知道get_five()是常量.因爲從理論上講, 這個函數可以影響(改變)一個全局變量或調用其它非運行時常量函數等.
     C++11引入了constexpr關鍵字, 允許用戶去保證一個函數或對象的構造函數是一個編譯期常量.上面的例子可以寫成下面這樣:

constexpr int get_five() {return 5;} 
int some_value[get_five() + 7]; // 創建一個包含12個整數的數組. 這種形式在C++11中是合法的.

  這樣可以讓編譯器理解並驗證get_five()是一個編譯期常量!作用在函數上的constexpr關鍵字對函數的行爲施加了一些限制. 首先, 這個函數的返回值類型不能是void; 其次, 在函數體中不能聲明變量或新類型; 第三, 函數體內只能包含聲明語句,空語句和單個return語句且,return語句中的表達式也必須是常量表達式.
  在c++11之前, 變量的值只有在變量被聲明爲const類型,有常量表達式初始化它, 並且是整型或枚舉類型時才能用在常量表達式中. C++11去掉必須是整數或枚舉的限制,如果定義變量時用了constexpr關鍵字:

constexpr double earth_gravitational_acceleration = 9.8;
constexpr double moon_gravitational_acceleration = earth_gravitational_acceleration / 6.0;

  這種數據變量是隱式的常量, 必須用常量表達式初始化.想要構造用戶定義類型的常量值,構造函數也必須用constexpr聲明.

    3.3 對POD定義的修正                                                        在C++03中, 類或結構體必須遵守幾條規則才能認爲是POD類型.符合這種定義的類型能夠產生與C兼容的對象(內存)佈局, 而且可以被靜態初始化.C++03標準對與C兼容或可以靜態初始化的類型有嚴格的限制,儘管不是因技術原因導致編譯器不接受這樣的編碼.如果創建了一個C++03的POD類型,又想要增加一個非虛成員函數, 那麼這個類型就不再是POD類型了, 不能靜態初始化,並且與C不兼容了, 儘管沒有改變內存佈局.

     C++11將POD的概念拆分成了兩個獨立的概念:平凡的(trivial)和標準佈局(standard-layout), 放寬了幾條POD規則. 一個平凡的(trivial)類型可以靜態初始化. 這意味着可以通過memcpy來拷貝數據,而不必通過拷貝構造函數. 平凡類型的變量的生命期是從分配存儲空間開始的,而不是從構造完成開始.

    平凡的類和結構體定義如下:

      1 有一個平凡的默認構造函數. 這可以使用默認構造函數語法, 例如SomeConstructor() = default;

    2 有平凡的拷貝和移動構造函數, 可以使用默認語法.

    3 有平凡的拷貝和移動賦值運算符, 可以使用轉變語法.

    4 有一個平凡的析構函數, 必須是非虛函數.

   只有當一個類沒有虛函數,沒虛基類時它的構造函數纔是平凡的. 拷貝和移動操作還要求類的所有非靜態數據成員都是平凡的.

   一個類型是標準佈局的,就意味着它將以與C兼容的方式來排列和打包它的成員.標準佈局的類和結構體定義如下:

    1 沒有虛函數

    2 沒有虛基類

    3 所有的非靜態數據成員都有相同的訪問控制 (public, private, protected)

    4 所有的非靜態數據成員, 包括基類中的, 都要在繼承體系中的同一個類中.

    5 以上規則也適用於類體系中的所有基類和所有非靜態數據成員.

    6 沒有和第一個定義的靜態數據成員相同類型的基類

    如果一個類型是平凡的(trivial),是標準佈局的, 並且所有的非靜態數據成員和基類都是POD類型的, 那麼這個類型就是POD類型.                                                                                                                                                                        

  4 核心語言建構期性能提高

   4.1 外部模板                                                                在C++03中,只要在編譯單元內遇到被完整定義的模板,編譯器都必須將其實例化(instantiate)。這會大大增加編譯時間,特別是模板在許多編譯單元內使用相同的參數實例化。沒有辦法告訴C++不要引發模板的實例化.C++11引入了外部模板聲明, 就像外部數據聲明一樣. C++03用下面的語法迫使編譯器實例化一個模板:

    template class std::vector<MyClass>;
    而C++11提供下面的語法告訴編譯器在當前編譯單元中不要實例化這個模板:

    extern template class std::vector<MyClass>;

  5 核心語言可用性的增強

  這些特性存在的主要目的是爲了讓C++更使用. 這些特性可以改進類型安全, 最小化代碼重複, 儘可能減少錯誤代碼等.                                                                                                                                      

  5.1 初始化列表

  C++03從C語言繼承了初始化列表這一特性. 在一對大括號中列出參數的方式來給出一個結構體或者數組, 這些參數值按照各個成員在結構體中的定義順序來排列.這些初始化列表是遞歸的,所以一個結構體數組或包含另一個結構體的結構體可以使用它們.

  1. struct Object {  
  2. float first;  
  3. int second;  
  4. };  
  5.   
  6. Object scalar = {0.43f, 10}; //One Object, with first=0.43f and second=10  
  7. Object anArray[] = {{13.4f, 3}, {43.28f, 29}, {5.934f, 17}}; //An array of three Objects  

   這對於靜態初列表或者只想把結構體初始化爲某個特定值而言是非常有用的. C++提供了構造函數來初始化對象, 但這沒有初始化列表方便.C++03只允許符合POD定義的類型使用初始化列表,非POD的類型不能使用,就連相當有用的STL容器std::vector也不行.C++11擴展了初始化列表, 使用它可以用在所有類上,包括像vector這樣的標準容器.
   C++11把初始化列表的概念綁到一個叫做std::initializer_list的模板上.這允許構造函數或其他函數將初始化列表做爲參數.例如:

  1. class SequenceClass {  
  2. public:  
  3. SequenceClass(std::initializer_list list);  
  4. };  

  這使得可以從一串整數來創建SequenceClass對象, 例如:
      SequenceClass some_var = {1, 4, 5, 6};
      這個構造函數是一種特殊的構造函數,叫做初始化列表構造函數(initializer-list-constructor).有這種構造函數的類在統一初始化中會被特殊對待(詳見5.2)
類型std::initializer_list<>是個第一級的C++11標準程序庫類型. 但是,它們只能由C++11通過{}語法來靜態構造!這個列表一經構造便可複製,雖然這只是copy-by-reference.初始化列表是常數;一旦被創建,其成員均不能被改變,成員中的數據也不能夠被改變.
  因爲初始化列表是真實類型,除了構造函數之外還能夠被用在其他地方。常規的函數能夠使用初始化列表作爲參數。例如: 

  1.    void FunctionName(std::initializer_list list);  
  2.  FunctionName({1.0f, -3.45f, -0.4f});  
  3.  標準容器也能夠以這種方式初始化:  
  4. std::vector v = { "xyzzy""plugh""abracadabra" };  
  5. std::vector v({ "xyzzy""plugh""abracadabra" });  
  6. std::vector v{ "xyzzy""plugh""abracadabra" }; // 參見下面 "統一的初始化"<span style="font-family: verdana, 'ms song', Arial, Helvetica, sans-serif;"> </span>  

  5.2 統一的初始化

  C+03在初始化類型方面有着許多問題.初始化類型有數種方法,而且交換使用時不會都產生相同結果。傳統的建構式語法,看起來像是函數聲明,而且必須採取一些步驟保證不破壞編譯器那些最讓人惱火的解析規則.只有聚合體和POD類型能夠用集合式初始化(通過SomeType var = {}; 形式的語法)
    C++11提供了一個完全統一的可以用在任何類型的對象的初始化語法. 它擴展了初始化列表語法:

  1. struct BasicStruct {  
  2.    int x;  
  3.    double y;  
  4.   };  
  5.     
  6.   struct AltStruct {  
  7.    AltStruct(int x, double y) : x_{x}, y_{y} {}  
  8.     
  9.   private:  
  10.    int x_;  
  11.    double y_;  
  12.   };  
  13.     
  14.   BasicStruct var1{5, 3.2};  
  15.   AltStruct var2{2, 4.3};  

  var1初始化行爲就像聚合初始化一樣.也就是說,每個數據成員就是一個對對象, 按順序從初始化列表中拷貝一個對應的值來初始化它們.如果有需要, 會進行隱式類型轉換.如果存在向下類型轉換(轉換後的數據類型不能表示原數據類型,轉換後可能有數據丟失,例如將unsigned轉換成int), 那麼這個程序就是病態的,會導致編譯失敗. var2的初始化則是簡單地調用構造函數.
  統一的初始化還可做下面這件事:

  1. struct IdString   
  2. {  
  3.    std::string name;  
  4.    int identifier;  
  5. };  
  6.     
  7. IdString get_string()   
  8. {  
  9.    return {"foo", 42}; //注意,這裏沒有指定具體類型.  
  10. }  

  統一初始化不會取代構造函數語法,還是有一些時候是需要構造函數語法的.如果一個類有初始化列表構造函數(TypeName(initializer_list);),假定它有資格成爲構造函數之一(我們知道,一個類可以有多個構造函數),那麼它的優先級會高於其它形式的構造函數.C++11版本的std::vector就有一個初始化列表構造函數.這意味着  std::vector the_vec{4};會調用初始化列表構造函數,而不是調用以vector大小爲唯一參數的構造函數. 要訪問後一個構造函數, 用戶必須直接使用標準構造函數語法.

  5.3 類型推導

  在C++03(還有C)中,必須顯式指定變量的類型.然而,隨着模板類型和模板元編程技術的出現,某些東西的類型,尤其是函數的返回類型,可能不是那麼容易表示的了. 在這種情況下,將中間結果存儲在某個變量中是件很困難的事情.可能需要去了解特定的模板元編程庫的內部實現.
     C++11提供兩種方法來緩解上述問題. 一,定義有顯式初始化的變量可以用auto關鍵字來自動確定變量類型,這將用初始化表達式的類型來創建變量:

  1. auto some_strange_callable_type = boost::bind(&some_function, _2, _1, some_object);  
  2. auto other_variable = 5;  

  some_strange_callable_type的類型很簡單, 就是boost::bind模板函數返回值的類型.作爲編譯器語義分析責任的一部份,編譯很容易確定這個類型,但程序員就沒那麼容易確定了.otherVariable 的類型同樣也是定義明確的,程序員很容易就能判別。它是個int(整數),就和整數字面值的類型一樣。

  另外,關鍵字decltype可以用來在編譯期確定表達式的類型.例如:

  1. int some_int;  
  2. decltype(some_int) other_integer_variable = 5;  

  decltype 和 auto 一起使用會更爲有用,因爲 auto 參數的類型只有編譯器知道.然而 decltype對於那些大量運用運算符重載和類型特化來編碼的表達式非常有用。auto對減少代碼冗餘也很有用.比如說, 程序員不用像下面這樣寫代碼:

  1. for (std::vector::const_iterator itr = myvec.cbegin(); itr != myvec.cend(); ++itr)  
  2. //而可以用更簡短的形式:  
  3. for (auto itr = myvec.cbegin(); itr != myvec.cend(); ++itr)  

  這兩種形式的差距會隨着你使用的容器的嵌套層次而增加, 這種情況下typedef也是一種減少代碼的好方法!由decltype得出的類型可以和由auto推導出的類型不同:

  1. #include<vector>  
  2. int main() {  
  3. const std::vector v(1);  
  4. auto a = v[0];        // a 是 int 類型  
  5. decltype(v[1]) b = 1; // b 是 const int& 類型, 是std::vector::operator[](size_type) const  
  6.                       // 的返回類型  
  7. auto c = 0;           // c 是 int 類型  
  8. auto d = c;           // d 是 int 類型  
  9. decltype(c) e;        // e 是 int 類型, c變量的類型  
  10. decltype((c)) f = c;  // f 是int&類型, 因爲(c)是一個左值  
  11. decltype(0) g;        // g 是 int 類型, 因爲0是一個右值  

  5.4 基於範圍的for循環

  在C++03中,要遍歷一個list中的元素需要很多代碼.其它語言實現支持"糖塊句法",允許程序通過一個簡單的"foreach"語句自動遍歷list中的元素.其中之一就是Java語言, 它從5.0開始支持增強的for循環.
     C++11增加了一個類似的特性, for語句可以簡單地遍歷列表中的元素.

  1. int my_array[5] = {1, 2, 3, 4, 5};  
  2. // double the value of each element in my_array:  
  3. for (int &x : my_array)   
  4. {  
  5.     x *= 2;  
  6. }  

  這種形式的for語句叫作"基於範圍的for語句",它會遍歷列表中的每一個元素.可以用在C風格數組,初始化列表和那些帶有能返回迭代器的begin()和end()函數的類型上.所有提供了begin/end的標準容器都可以使用基於範圍的for語句.

  5.5 Lamda函數與表達式

  C++11提供了創建匿名函數的能力,叫做Lamda函數. 具體內容請參考: http://www.cnblogs.com/pzhfei/archive/2013/01/14/lambda_expression.html

       5.6 一種新的函數語法

  標準C的函數聲明語法對C語言的特性集而言完全足夠了. 因爲C++從C發展而來, 保留了C的基本語法並在需要的地方進行了擴展. 然而,C++的結構變得更加複雜了,暴露出了很多的侷限性,尤其是模板函數的聲明.下面的例子在C++03中是不允許的:

  1. template  
  2. Ret adding_func(const Lhs &lhs, const Rhs &rhs) {return lhs + rhs;} //Ret must be the type of lhs+rhs  

  Ret的類型是lhs+rhs的結果的類型.就算用前面提到的C++11中的decltype,也是不行的:

  1. template  
  2. decltype(lhs+rhs) adding_func(const Lhs &lhs, const Rhs &rhs) {return lhs + rhs;} //Not legal C++11  

  這不是合法的C++,因爲lhs和rhs還沒定義;解析器解析完函數原型的剩餘部分之前,它們還不是有效的標識符.
  爲此, C++11引入了一種新的函數聲明語法,叫做後置返回類型(trailing-return-type).

  1. template  
  2. auto adding_func(const Lhs &lhs, const Rhs &rhs) -> decltype(lhs+rhs) {return lhs + rhs;}  

  這種語法可以用到更普通的函數聲明和定義上:

  1.  struct SomeStruct  {  
  2.      auto func_name(int x, int y) -> int;  
  3.  };  
  4.   
  5. auto SomeStruct::func_name(int x, int y) -> int {  
  6. return x + y;  
  7. }  

  關鍵字auto的這種用法與在自動類型推導中有所不同.

5.7 對象構造的改進

C++03中類的構造函數不允許調用該類的其它構造函數;每個構造函數都必須自己或者調用一個公共的成員函數來構造類的全部成員.例如:

  1. class SomeType  {  
  2.     int number;  
  3.    
  4. public:  
  5.     SomeType(int new_number) : number(new_number) {}  
  6.     SomeType() : number(42) {}  
  7. };  
  8. class SomeType  {  
  9.     int number;  
  10.    
  11. private:  
  12.     void Construct(int new_number) { number = new_number; }  
  13. public:  
  14.     SomeType(int new_number) { Construct(new_number); }  
  15.     SomeType() { Construct(42); }  
  16. };  

  而且,基類的構造函數不能直接暴露給派生類;每個派生類必須實現自己的構造函數哪怕基類的構造函數已經夠用了.非靜態數據成員不能在聲明的地方初始化.它們只能在構造函數中初始化.

  C++11爲這些問題提供瞭解決方案.C++11允許構造函數調用另一個構造函數(叫做委託構造).這允許構造函數利用其它構造函數的行爲而只需增加少量的代碼.C#,java和D語言都提供了這種功能. C++的語法如下:

  1. class SomeType  {  
  2.     int number;  
  3.    
  4. public:  
  5.     SomeType(int new_number) : number(new_number) {}  
  6.     SomeType() : SomeType(42) {}  
  7. };  

  注意:這個例子可以通過給new_number設定一個默認參數來達到相同的效果.但是,這種新語法可以讓這個默認值在實現中來設置而不是在接口中設置.這帶來的一個好處就是,對庫代碼的維護者而言,在接口中(頭文件中)聲明默認值,這個默認值被嵌入到了調用端;要改變這個默認值的話,調用端的代碼都需要重新編譯.但委託構造可以在實現中(CPP文件中)來改變這個默認值, 這樣調用端的代碼就不需要重新編譯,只用重新編譯這個庫就可以了.
    還有一個附加說明: C++03認爲,構造函數執行完了一個對象就被構造好了. 而C++11則認爲,只要任何一個構造函數執行完了,對象就算構造完成了. 由於可能有多構造函數會被執行,C++11的這種做法就意味着,所有的委託構造函數都將在一個已經用它自己類型完全構造好的對象上執行.這句話什麼意思呢?舉個例子, B 繼承自 A, B的構造函數裏調用了A的構造函數;當A的構造函數執行完以後,就已經有一個A類的對象構造完成了.而這時B的構造函數不會再構造一個新對象,而是把那個A對象改造成B類的對象(這是我的推測).再舉一個例子,類C有兩個構造函數C1和C2, C2調用了C1. 當C1執行完後,已構造好了一個C類對象.而這時C2的代碼會直接作用在這個對象上,不會再構造一個新對象.C++03就會構造2個對象,其中一個是臨時對象.
    對於基類的構造函數,C++11允許一個類指定要不要繼承基類的構造函數.注意,這是一個"全部"或"全不"的特性,要麼繼承基類的全部構造函數,要麼一個都不繼承.
    此外,對多重繼承有一些限制,從多個基類繼承而來的構造函數不可以有相同的函數簽名(signature).而派生類的新加入的構造函數也不可以和繼承而來的基類構造函數有相同的函數簽名,因爲這相當於重複聲明.語法如下:

  1. class BaseClass {  
  2. public:  
  3.     BaseClass(int value);  
  4. };  
  5.    
  6. class DerivedClass : public BaseClass {  
  7. public:  
  8.     using BaseClass::BaseClass;  
  9. };  
  10. 對於成員初始化,C++11允許下面這樣的語法:  
  11. class SomeClass {  
  12. public:  
  13.     SomeClass() {}  
  14.     explicit SomeClass(int new_value) : value(new_value) {}  
  15.    
  16. private:  
  17.     int value = 5;  
  18. };  

  每一個構造函數都將把value初始化爲5, 如果它們沒用其它值來覆蓋這個初始化的話.上面那個空的構造函數會把value初始化爲類定義時的狀態5.而那帶有參數的構造函數會用指定的值來初始化value.成員的初始化也可以使用前面提到的統一初始化.

5.8 顯式重寫(覆蓋,override)和final   

在C++03中,很容易讓你在本想重寫基類某個函數的時候卻意外地創建了另一個虛函數.例如:

  1. struct Base {  
  2.     virtual void some_func(float);  
  3. };  
  4.    
  5. struct Derived : Base {  
  6.     virtual void some_func(int);  
  7. };  

  本來Derived::some_func函數是想替代Base中那個函數的.但是因它的接口不同, 又創建了一個虛函數.這是個常見的問題, 特別是當用戶想要修改基類的時候.
  C++11引入了新的語法來解決這個問題:

  1. struct Base {  
  2.     virtual void some_func(float);  
  3. };  
  4.    
  5. struct Derived : Base {  
  6.     virtual void some_func(int) override; // 病態的,不會重寫基類的方法  
  7. };  

  override 這個特殊的標識符意味編譯器將去檢查基類中有沒有一個具有相同簽名的虛函數,如果沒有,編譯器就會報錯!
  C++11還增加了防止基類被繼承和防止子類重寫函數的能力.這是由特殊的標識符final來完成的,例如:

  1. struct Base1 final { };  
  2.    
  3. struct Derived1 : Base1 { }; // 病態的, 因爲類Base1被標記爲final了  
  4.    
  5. struct Base2 {  
  6.     virtual void f() final;  
  7. };  
  8.    
  9. struct Derived2 : Base2 {  
  10.     void f(); // 病態的, 因爲虛函數Base2::f 被標記爲final了.  
  11. };  

  在這個例子中, virtual void f() final;語句聲明瞭一個虛函數卻也阻止了子類重寫這個函數.它還有一個作用,就是防止了子類將那個特殊的函數名與新的參數組合在一起.
  需要注意的是,override和final都不是C++語言的關鍵字.他們是技術上的標識符,只有在它們被用在上面這些特定的上下文在纔有特殊意義.用在其它地方他們仍然是有效標識符.

5.9 空指針常量

  本節中出的"0"都將解釋爲"一個求值結果爲0的int型常量表達式". 實際上任何整數類型都可以作爲常量表達式.
  自從1972年C誕生以來,常量0就有着int型常量和空指針的雙重角色.C語言用預處理宏NULL來處理這個固有的歧義, NULL通常被定義爲(void*)0或0.而C++不採用同樣行爲,只允許0做空指針常量.而這與函數重載配合時就顯得有些弱智了.

  1. void foo(char *);  
  2. void foo(int);  

  如果NULL定義爲0,那麼foo(NULL);語句將會調用foo(int).這幾乎必定不是程序員想要的,也不是代碼直觀上要表達的意圖.
     C++11通過引入一個新的關鍵字nullptr充當單獨的空指針常量來糾正這個問題.它的類型是nullptr_t,是一個可以隱式轉換任意類型的指針或指向成員的指針的類型,並且可以和這些類型進行比較.它不能隱式轉換爲整型,也不能與整型做比較,bool類型除外.儘管最初的提議中一個nullptr類型的右值不應該能轉換爲bool類型,但是爲了保持與常規指針類型的一致性,核心語言工作組還是認定這種轉換是合理的. 爲了向下兼容,0仍然是一個有效的空指針常量!

  1. char *pc = nullptr;     // OK  
  2. int  *pi = nullptr;     // OK  
  3. bool   b = nullptr;     // OK. b is false.  
  4. int    i = nullptr;     // error  
  5.    
  6. foo(nullptr);           // calls foo(char *), not foo(int);<span style="background-color: rgb(255, 255, 255); font-family: verdana, 'ms song', Arial, Helvetica, sans-serif;"> </span>  

5.10 強類型枚舉

  在C++03中,枚舉不是類型安全的.他們實際上是整數,儘管他們是不同的枚舉類型.這使得我們可以比較兩種不同類型的枚舉值.C++03提供的唯一安全性就是,一個整數或一個枚舉類型的值不能隱式地轉換成另一個枚舉類型.此外,底層的具體的整數類型(short,long,int,...)是由實現(編譯器)定義的,標準並無明確規定.因此,那些枚舉變量的大小的代碼將是不可移植的.最後,枚舉值是暴露在外層作用域(直接包含枚舉定義的作用域)中的.所以,兩個不同枚舉類型的成員不可能有相同的名字.
  C++11引入了一個沒上述問題的特殊"枚舉類".使用 enum class(也可以用同義詞enum struct)來聲明:

  1. enum class Enumeration {  
  2.     Val1,  
  3.     Val2,  
  4.     Val3 = 100,  
  5.     Val4 // = 101  
  6. };  

  這種枚舉是類型安全的;枚舉值不能隱式地轉換成整數,所以也不可以和整數做比較.表達式 Enumeration::Val4 == 101會報一個編譯錯誤.
枚舉類的底層類型總是已知的.默認是int型,這可以用其它整數類型來覆蓋它.就像下面這個例子:

  1. enum class Enum2 : unsigned int {Val1, Val2};  

  老式的枚舉被放在直接包含該定義的作用域中.新式的枚舉被放在枚舉類的作用中.所以,上例中Val1是未定義的,而Enum2::Val1是已定義的.
  C++11還提供了一個過渡語法讓老式的枚舉類型可以提供顯式的作用域和定義底層整數類型.語法如下:

  1. enum Enum3 : unsigned long {Val1 = 1, Val2};  

  這個例子中枚舉名字被定義在枚舉類型的作用域內(Enum3::Val1),但是爲了向下兼容它們也會被放在直接包含在Enum3所在的作用域中.

5.11 右尖括號

C++03的解析器都把">>"定義爲右移運算符.但是,在嵌套的模板聲明中,程序員往往傾向於忽略兩個右尖括號之間的空格.這會導致編譯器報一個語法錯誤.
C++11改進了編譯器的解析規則,儘可能地將多個右尖括號(>)解析成模板參數結束符.可以用圓括號來改變這個規則,圓號的優先級比它高.例如:

  1. template<bool Test> class SomeType;  
  2. std::vector<SomeType<1>2>> x1;  // 被解析成 std::vector of SomeType<true> 2>,  
  3. // 這是錯誤的語法, 1 被當成 true 了.  
  4. std::vector<SomeType<(1>2)>> x1;  // 被解析成 std::vector of SomeType<false>,  
  5. // 在C++11中是合法的. (1>2) 這個表達式的結果爲false.  

5.12 顯式類型轉換操作符

    C++98 增加了explicit關鍵字來防止單參數的構造函數被用作隱式的類型轉換操作符.然而,卻沒有對真正的類型轉換操作符這樣做.例如,智能指針可能定義了operator bool(),讓它的行爲更像真原始指針.有了這個轉換操作符就可以用if語句來測試:if (smart_ptr_variable),當這個指針不爲空時結果爲真,否則爲假.但是,這也會引起其他一些非預期的轉換操作.因C++中的bool被定義爲一種算術類型,可以隱式地轉換爲整數甚至是浮點數進行數學運算. 拿轉換出的布爾值進行非布爾計算的數學計算,往往不是程序員想要的.
  C++11中,explicit關鍵字也可以用在類型轉換操作符上.和用在構造函數上一樣,防止這種轉換函數被於隱式類型轉換.當然了,在語言的上下文明確要求使用布爾變量(如if語句和循環語句中的條件,還有邏輯運算符的操作數)的地方,會被當作顯示轉換.所以可以使用類型轉換操作符.

    5.13 模板的別名

  在進入這個主題前,先弄清楚"模板"和"類型"的區別.類型,是具體的數據類型,可以直接用來定義變量. 模板,是類型的模板,根據這個模板可以產生具體的類型;模板是不能直接定義變量的;當指定了所有的模板參數後,就產生了一個具體的類型,就可以用來定義變量了.
  在C++03中,只能爲類型(包括完全特化的模板,也是一種類型)定義別名, 而不能爲模板定義別名:  

  1. template <typename First, typename Second, int Third>  
  2. class SomeType;  
  3.    
  4. template <typename Second>  
  5. typedef SomeType<OtherType, Second, 5> TypedefName; // 在C++03中, 這是非法的.  

C++11增加爲模板定義別名的能力,用下面這樣的語法:

  1. template <typename First, typename Second, int Third>  
  2. class SomeType;  
  3.    
  4. template <typename Second>  
  5. using TypedefName = SomeType<OtherType, Second, 5>;  

  這種using語法也可以用來定義類型的別名:  

  1. typedef void (*FunctionType)(double);       // 老式語法  
  2. using FunctionType = void (*)(double); // 新式語法<span style="background-color: rgb(255, 255, 255); font-family: verdana, 'ms song', Arial, Helvetica, sans-serif;"> </span>  

5.14 無限制的unions

  C++03中,對哪些類型的對象能夠作爲聯合的成員是有限制的.例如,聯合不能包含定義了非平凡構造函數的對象.C++11廢除了其中的一些限制:
現在,聯合可以包含定義了非平凡構造函數的對象;如果包含了,那麼聯合就必須要顯式定義一個構造函數.

  1. #include <new> // Required for placement 'new'.  
  2.    
  3. struct Point {  
  4.     Point() {}  
  5.     Point(int x, int y): x_(x), y_(y) {}  
  6.     int x_, y_;  
  7. };  
  8.    
  9. union U {  
  10.     int z;  
  11.     double w;  
  12.     Point p; // 非法的C++03; 合法的C++11.  
  13.     U() {new(&p) Point();} // 由於Point的原因, 必須定義構造函數.  
  14. };  

  因爲是放寬了現有的規則,所以不會對已有的代碼造成影響. 

6 核心語言功能的改進

 這些特性讓C++語言可以完成那些以前不可能的,極其繁瑣的或者需要一些不可移植的庫才能完成的事情.

6.1 可變參數模板  

  在 C++11 之前, 不論是類模板或是函數模板,都只能按其被聲明時所指定的樣子,接受一組數目固定的模板參數.C++11 加入新的表示法,允許任意個數,任意類型的模板參數,不必在定義時將參數的個數固定。

  1. template<typename... Values> class tuple;  

模板類 tuple 的對象,能接受不限個數的 typename 作爲它的模板形參:

  1. class tuple<int, std::vector<int>, std::map<std::string, std::vector<int>>> someInstanceName;  

實參的個數也可以是0,所以 class tuple<> someInstanceName 這樣的定義也是可以的。

若不希望產生實參個數爲 0 的變長參數模板,則可以採用以下的定義:

  1. template<typename First, typename... Rest> class tuple;  

  變長參數模板也能運用到函數模板上。傳統 C 中的 printf 函數,雖然也能做到不定個數的形參來調用,但其並非類型安全的。以下的例子中,C++11 除了能定義類型安全的變長參數函數外,還能讓類似 printf 的函數能自然地處理自定義類型的對象。 除了在模板參數中能使用...表示不定長模板參數外,函數參數也使用同樣的表示法代表不定長參數。

  1. template<typename... Params> void printf(const std::string &strFormat, Params... parameters);  

  其中,Params 與 parameters 分別代表模板與函數的變長參數集合,稱之爲參數包 (parameter pack).參數包必須要和運算符"..."搭配使用,避免語法上的歧義。

  變長參數模板中,無法像在類或函數中那樣使用參數包.因此典型的做法是以遞歸的方法取出可用參數,請看以下的 C++11 printf 例子:

  1. void printf(const char *s)  
  2. {  
  3.   while (*s)  
  4.   {  
  5.     if (*s == '%' && *(++s) != '%')  
  6.       throw std::runtime_error("invalid format string: missing arguments");  
  7.     std::cout << *s++;  
  8.   }  
  9. }  
  10.    
  11. template<typename T, typename... Args>  
  12. void printf(const char* s, T value, Args... args)  
  13. {  
  14.   while (*s)  
  15.   {  
  16.     if (*s == '%' && *(++s) != '%')  
  17.     {  
  18.       std::cout << value;  
  19.       printf(*s ? ++s : s, args...); // 即便當 *s == 0 也會產生調用,以檢測更多的類型參數。  
  20.       return;  
  21.     }  
  22.     std::cout << *s++;  
  23.   }  
  24.   throw std::logic_error("extra arguments provided to printf");  
  25. }  

  printf 會不斷地遞歸調用自身:函數參數包 args... 在調用時, 會被模板類匹配分離爲 T value和 Args... args.直到 args... 變爲空參數,則會與簡單的 printf(const char *s) 形成匹配,退出遞歸。

  另一個例子爲計算模板參數的個數,這裏使用相似的技巧展開模板參數包 Args...:

  1. template<>  
  2. struct count<> {  
  3.     static const int value = 0;  
  4. };  
  5.    
  6. template<typename T, typename... Args>  
  7. struct count<T, Args...> {   
  8.     static const int value = 1 + count<Args...>::value;  
  9. };  

  雖然沒有一個簡潔的機制能夠對變長參數模板中的值進行迭代,但使用運算符"..."還能在代碼各處對參數包施以更復雜的展開操作。舉例來說,一個模板類的定義如下:

  1. template <typename... BaseClasses> class ClassName : public BaseClasses...  
  2. {  
  3. public:  
  4.    
  5.    ClassName (BaseClasses&&... baseClasses) : BaseClasses(baseClasses)... {}  
  6. }  

  BaseClasses... 會被展開成類型 ClassName 的基底類; ClassName 的構造函數需要所有基類的左值引用,而每一個基類都是以傳入的參數做初始化 (BaseClasses(baseClasses)...)。

  在函數模板中,變長參數可以和左值引用搭配,達成形參的完美轉發 (perfect forwarding):

  1. template<typename TypeToConstruct> struct SharedPtrAllocator  
  2. {  
  3.   template<typename... Args> std::shared_ptr<TypeToConstruct> ConstructWithSharedPtr(Args&&... params)  
  4.   {  
  5.     return tr1::shared_ptr<TypeToConstruct>(new TypeToConstruct(std::forward<Args>(params)...));  
  6.   }  
  7. }  

  參數包 parms 可展開爲 TypeToConstruct 構造函數的形參。 表達式std::forward<Args>(params) 可將形參的類別信息保留(利用右值引用),傳入構造函數。 而運算符"..."則能將前述的表達式套用到每一個參數包中的參數。這種工廠函數(factory function)的手法, 使用 std::shared_ptr 管理配置對象的內存,避免了不當使用所產生的內存泄漏(memory leaks)。

  此外,變長參數的數量可以藉以下的語法得知:

  1. template<typename ...Args> struct SomeStruct  
  2. {  
  3.   static const int size = sizeof...(Args);  
  4. }  

  SomeStruct<Type1, Type2>::size 是 2,而 SomeStruct<>::size 會是 0。 (sizeof...(Args) 的結果是編譯期常數。)

6.2 新的字符串字面值

  C++03提供兩種字符串字面值.第一種,包含在一對雙引號內,產生一個以空字符結尾的const char數組.第二種,由L""定義,產生以空字符結尾的const wchar_t類型的數組.wchar_t是一個大小和定義都未明確定義的寬字符.字符串字面值既不支持UTF-8,UTF-16也不支持其它任何類型的unicode編碼.
  char類型的定義被修改了,明確表述爲:char的大小至少能存儲UTF-8的8位編碼,並且要足夠大到能夠存儲編譯器實際使用的字符集的任何成員.這以前只在C++標準的後半部分中有定義,依靠C標準來保證char的大小至少爲8位.
  C++11支持3種UNICODE編碼: UTF-8, UTF-16, 和 UTF-32.除了前面提到的char類型定義的修改,C++11還增加了兩種字符類型:char16_t 和 char32_t.這兩種類型是分別用來存儲UTF-16和UTF-32的.
   下面的例子展示瞭如何創建各種編碼類型的字符串字面值:

u8"I'm a UTF-8 string."
u"This is a UTF-16 string."
U"This is a UTF-32 string."

  第一個字符串的類型是const char[], 第二個字符串的類型是const char16_t[], 第三個字符串的類型是const char32_t[]. 創建Unicode字符串時,經常會直接插入Unicode編碼值到字符串中.爲此,C++11提供如下語法:

u8"This is a Unicode Character: \u2018."
u"This is a bigger Unicode Character: \u2018."
U"This is a Unicode Character: \U00002018."

  '\u'後面是一個16進制數字,不需要加0x前綴.標識符\u代表一個16位的Unicode碼點.要輸入32位的碼點,使用\U加上32位的16進制數.只能輸入有效的Unicode碼點.例如,U+D800—U+DFFF之前的碼點是被禁止的,因爲他們被保留用作UTF-16編碼中的代理對.

  有時候我們需要手動避免轉義某些字符,尤其是在使用xml文件,腳本語言或正則表達式等的字符串字面值時.C++提供了原始字符串:

R"(The String Data \ Stuff " )"
R"delimiter(The String Data \ Stuff " )delimiter"

  6.3 用戶自定義的字面值

  C++03提供幾種字面值.字符串"12.5"會被編譯器解析爲double類型的值12.5. 但是,帶有'f'後綴的字符串"12.5f"會創建一個float類型的值12.5. 後綴修飾符已經被C++標準固定下來了, 用戶代碼不能增加新的修飾符!
  C++11增加了讓用戶定義新的字面值修飾符的能力, 新的修飾符會基於被修飾的字符串來構造對象.
  字面值的轉換可以分爲兩個階段:原始的和轉換後的(raw and cooked).原始的字面值是指某種特定類型的字符序列, 而加工過的字面值則代表另一種類型.C++的字面值1234, 原始的字面值就是字符序列'1','2','3','4';而轉換後的字面值是整數1234. C++字面值0xA,轉換前的字面值是'0','x','A',轉換後就是整數10.字面值原始和轉換後的形式都可以被擴展.但字符串除外,它只有轉換後的形式可以被擴展.這個例外是因爲考慮到字符串有着會影響字符的特定意義和類型的前綴. 所有的用戶自定義字面值都是加後綴的, 想定義加前綴的字面值是不可能的.
  自定義字面值原始形式的處理定義如下:

OutputType operator "" _suffix(const char * literal_string); 
OutputType some_variable = 1234_suffix;

  第二個語句執行由自定義字面值定義的函數代碼.
  另一種處理整數和浮點數原始字面值的機制是通過可變參數模板:

template<char...> OutputType operator "" _tuffix();
 
OutputType some_variable = 1234_tuffix;
OutputType another_variable = 2.17_tuffix;

  這個例子展示瞭如何以operator "" _tuffix<'1', '2', '3', '4'>()函數處理字面值.這種形式中,字符串沒有結尾的空字符!這麼做的主要目的是爲了使用C++11的constexpr關鍵字和編譯器能在編譯期就完全轉換這些字面值.
  對於數字字面值,整數字面值轉換後的類型是unsigned long long,浮點數字面轉換後的類型是long double.沒有模板形式:

OutputType operator "" _suffix(unsigned long long);
OutputType operator "" _suffix(long double);
 
OutputType some_variable = 1234_suffix; // Uses the 'unsigned long long' overload.
OutputType another_variable = 3.1416_suffix; // Uses the 'long double' overload.

  對於字符串字面值,下面的例子用了前面提到的新的字符串後綴.

複製代碼
OutputType operator "" _ssuffix(const char     * string_values, size_t num_chars);
OutputType operator "" _ssuffix(const wchar_t  * string_values, size_t num_chars);
OutputType operator "" _ssuffix(const char16_t * string_values, size_t num_chars);
OutputType operator "" _ssuffix(const char32_t * string_values, size_t num_chars);
 
OutputType some_variable =   "1234"_ssuffix; // Uses the 'const char *' overload.
OutputType some_variable = u8"1234"_ssuffix; // Uses the 'const char *' overload.
OutputType some_variable =  L"1234"_ssuffix; // Uses the 'const wchar_t *'  overload.
OutputType some_variable =  u"1234"_ssuffix; // Uses the 'const char16_t *' overload.
OutputType some_variable =  U"1234"_ssuffix; // Uses the 'const char32_t *' overload.
複製代碼

 

6.4 多線程內存模型

  內存模型允許編譯器完成很重要的優化.即使像移動程序中的語句來合併循環這樣簡單的編譯器優化都能夠影響對潛在共享變量讀,寫操作的順序!改變讀寫順序會導致競態條件的產生.沒有內存模型,編譯器一般不能將這種優化應用到多線程程序中的,或者只能用於某些特殊情況.現代程序設計語言,比如Java,爲此實現了一個內存模型.內存模型指定了同步屏障(Synchronization Barriers),通過特殊的、定義好的同步操作(比如獲得一個進入同步塊或某方法的鎖)來建立的.內存模型規定,共享變量值的改變只需要對那些通過了同步屏障的線程是可見的.此外,競態條件這個概念的完整定義覆蓋了帶有內存屏障細節的操作順序. 這些語義給了編譯器更高的自由度去進行優化: 編譯器只需要確保優化前和優化後同步屏障內的變量(可能被共享)的值是一樣的.
  大多數關於內存模型的研究都是圍繞着以下主題進行的:
  設計一個能讓編譯器有最大的優化自由度,同時還能對自由競爭提供足夠保障的內存模型;
  提供關於這種內存模型的正確的程序優化. 
  C++11標準支持多線程編程.這包含兩個部分:同一個程序中允許有多個線程同時存在和庫支持線程之間的交互;內存模型定義了什麼時候多個線程能夠訪問同一個內存地址,並指定了什麼時候一個線程對內存的修改對另一個線程是可見的!(參見:7.2 線程設施).

6.5 線程局部存儲

  在多線程環境中,線程通常都有一些自己所獨有的變量. 函數的局部變量也是這樣, 但是全局變量和靜態變量就不一樣了.
  新的線程局部存儲的生存期(原有的靜態,動態,自動變量除外)由thread_local關鍵字指定. 靜態對象的生存期也可能會被thread-local生存期替代.這麼做的目的是讓thread-local(線程局部)生存期的對象可以像其他靜態對象一樣由構造函數創建,由析構函數銷燬.

  6.6 明確默認和明確刪除的特殊成員函數 

  C++03中,如果類沒有定義構造函數,拷貝構造函數,賦值函數和析構函數的話,編譯器會爲類提供這些函數.程序員可以自己定義這些函數來覆蓋編譯生成的默認版本.C++還定義了幾個可以作用在所有類上的操作符(比如,賦值操作符=,new操作符等),程序員也可以覆蓋它們.
  然而, 對這些默認函數的創建只有很少的控制.例如, 要生成一個不可拷貝的類必須要聲明私有的拷貝構造函數和私有的賦值操作符並且不定義它們的實現.試圖調用這些函數就會違反"一個定義原則"(ODR,一個函數可以被調用,那麼這個函數必須且只能有一個函數體定義).儘管診斷信息不是必須的,但是這類違規行爲可能會導致鏈接錯誤.
  就構造函數而言, 只要一個類定義了任意一個構造函數,編譯器就不會自動爲它生成構造函數了.這在很多情況下是很有用的,但有些情況下用戶定義了這些函數,編譯器還生成這些函數也是很有用的.
  C++11允許顯式指明要不要使用這些特殊的成員函數.例如,下面的聲明顯式指出要使用默認構造函數:

struct SomeType {
    SomeType() = default; //The default constructor is explicitly stated.
    SomeType(OtherType value);
};

  另一方面,一些特性可以被顯式地禁用.例如,下面的類是不可拷貝的:

struct NonCopyable {
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable & operator=(const NonCopyable&) = delete;
};

  指示符 = delete 可以用來阻止任何函數被調用,可以用來禁止調用帶特定參數的成員函數.例如:

struct NoInt {
    void f(double i);
    void f(int) = delete;    // 不能調用這個函數
};

  編譯器會拒絕試圖對帶int參數的函數f()的調用, 而不是默默地轉換爲對帶有double參數的f()的調用.這可以泛化到禁止除了帶double參數外其他任何參數類型的f()的調用.例如:

struct OnlyDouble {
    void f(double d);
    template<class T> void f(T) = delete;    //不能調用這個函數
};

 

6.7 long long int類型

  C++03中,最大的整數類型是long int.它保證使用的位數至少與int一樣. 這導致long int在一些實現是64位的, 而在另一些實現上卻是32位的.C++11增加了一個新的整數類型long long int來彌補這個缺陷.它保證至少與long int一樣大,並且不少於64位.這個類型早在C99就引入到了標準C中, 而且大多數C++編譯器都以擴展的形式支持這種類型了.

6.8 靜態斷言

  C++03提供兩種方法來測試斷言:宏assert和#error預處理指令.然而,這不適合用在模板中:宏在運行期間測試斷言,而預處理指令在編譯預處理階段測試斷言,這些都發生在模板實例化之前;也不適合用於依賴於模板參數的屬性.
  C++11引入了一個新的關鍵字static_assert在編譯期測試斷言.聲明呈現下面這樣的形式:

複製代碼
static_assert (constant-expression, error-message);
    //下面幾個例子展示怎樣使用static_assert:
static_assert((GREEKPI > 3.14) && (GREEKPI < 3.15), "GREEKPI is inaccurate!");
template<class T>
struct Check  {
    static_assert(sizeof(int) <= sizeof(T), "T is not big enough!");
};
template<class Integral>
Integral foo(Integral x, Integral y) {
    static_assert(std::is_integral<Integral>::value, "foo() parameter must be an integral type.");
}
複製代碼

  當常量表達的結果爲false時,編譯器就會產生一個錯誤消息.第一個例子類似於預處理指令#error,但是預處理指令只支持整數類型.相比之下,第二例子中的斷言在模板類Check每一次被實例化的時候都被檢查一次.
  除了模板之外,靜態斷言也是很有用的.例如:一個算法的某個實現依賴於long long類型必須大於int,這類型事情標準並沒有做出保證.這種假設在大多數系統和編譯器上是有效的,但絕不是全部!

6.9 允許sizeof運算符作用在類型的數據成員上,無須明確的對象

  C++03中,sizeof可以作用在類和對象上.但卻不能像下面這樣做:

struct SomeType { OtherType member; };
 
sizeof(SomeType::member); // C++03 不行. C++11 可以.
    //這會返回OtherType的大小.C++03不允許這樣做,會報一個編譯錯誤.C++11允許這樣做.

 

6.10 控制和查詢對象的對齊方式

  C++11可以用alignof和alingas來查詢和控制變量的對齊方式.

  alignof是一個操作符,他以一個類型爲參數,並且返回這個類型的實例必須分配的字節邊界值,這個值一定是2的整數次冪.如果參數是引用類型,那麼返回的是被引用的類型的對齊信息.對於數組,返回的是元素類型的對齊信息.
 laignas指示符變量的內存控制方式.這個指示符的參數是一個常量或一個類型, alignas(T)是alignas(alignof(T))的簡寫形式.例如:下面的例子聲明一個char數組,它的對齊方式與float型數據一樣.
  alignas(float) unsigned char c[sizeof(float)]

  6.11 允許實現垃圾回收

  之前版本的C++標準通過set_new_handler提供了程序員驅動的垃圾回收機制,但卻沒有爲自動化垃圾回收機制給出對象可到達性的定義. C++11定義了指針完全地從其他地方獲得值的條件.編譯器實現可以指定在嚴格的指針安全下進行操作,在這種情況下不按這個規則獲得值的指針就會變成無效的.

  6.12 屬性
  C++11爲編譯器和其他工具提供了標準的語言擴展語法.這些擴展歷來都是用#pragma指令或生產商指定的關鍵字(如GNU的__attributes__和微軟的 __declspec).C++11有了新的語法, 以雙重方括號的形式爲屬性指定額外的信息.屬性可以被用於各種代碼元素:

int [[attr1]] i [[attr2, attr3]];
 
[[attr4(arg1, arg2)]] if (cond)
{
    [[vendor::attr5]] return i;
}

   在上面的例子中,屬性attr1作用在變量i的類型int上,而attr2和attr3則作用於變量i本身.attr4作用於if語句,vendor::attr5作用於return語句.一般地(但有一些例外),爲一個命名實體指定的屬性放在實體名字之後,其他部分之前.多個屬性可以放在一個雙重方括號對中,像上面的例子那樣. 屬性可能會有附加的參數,屬性也可能被放在生產商指定的屬性命名空間中.
  建議屬性不要有任何語言上的意義,也不要改變程序的觀感. 屬性可以提供一些很有用的信息,例如幫助編譯器生成更好的診斷信息或優化生成的代碼.
  C++11本身提供兩種標準的屬性:noreturn屬性指出函數沒有返回值, carries_dependency屬性通過指出函數的參數或返回值有依賴關係來幫助優化多線程代碼.

7. C++標準程序庫的變化

  C++11標準庫引入了很多新特性.很多是在舊標準下實現的,但是有一些卻依賴於C++11的核心特性.新標準庫的大部分是在2005年公佈的C++標準委員會標準庫技術報告(tr1)中定義的.各種完全或部分的TR1實現在現行的標準中可以通過命名空間std::tr1來引用了.對於C++11,這些實現被移到了命名空間std中.然而,因爲TR1的特性被引入到了C++11的標準庫中,所以需要更新它們以適合那些在最初的TR1中不可用的C++11特性.
  C++11的標準化已經完成,標準委員會打算創建第二版標準庫技術報告(TR2).那些被提議但卻沒來及加入C++11的庫,將會被放入TR2或以後的技術報告中.

7.1 標準庫組件的升級

  C++11提供了很多現存標準庫組件能從中獲益的新特性.例如,大多數標準容器都可以從基於移動構造右值引用中獲益,不管是快速移動重型容器還是把容器的內容移動到新的內存位置.標準庫組件已經用適當的C++11新特性升級過了.包括但不限於以下特性:

  • 右值引用及其關聯的移動支持
  • 支持utf-16和utf-32編碼的unicode字符類型
  • 可變模板(加上右值引用可以實現完美轉發)
  • 編譯期常量表達式
  • decltype
  • 顯式類型轉換操作符
  • default/deleted成員函數

  7.2 線程支持

  C++11雖然從語言上提供了支持線程的內存模型,但主要的支持還是來自標準庫.
  新的標準庫提供了一個線程類(std::thread)來運行一個新線程,它帶有一個函數對象參數和一系列可選的傳遞給函數對象的參數.通過std::thread::join()支持的線程連接操作可以讓一個線程直到另一個線程執行完畢才停止.std:thread::native_handle()成員函數提供了對底層本地線程對象的可能且合理的平臺相關的操作.
  爲支持線程同步,標準庫增加了互斥體(std::mutex, std::recursive_mutex等)和條件變量(std::condition_variable 和std::condition_variable_any).這些都是通過RAII鎖和加鎖算法就可以簡單使用的.
  有時爲了高性能或底層工作,要求線程間的通信沒有開銷巨大的互斥鎖.原子操作可以達到這個目的,這可以隨意地爲一個操作指定最小的內存可見度.顯式的內存屏障也可以用於這個目的.
C++11線程庫還包含了futures和promises,用於在線程間傳遞異步結果.並且提供了std::packaged_task來封裝可以產生這種異步結果的函數調用.
  更高級的線程支持,如線程池,已經決定留待在未來的 Technical Report 加入此類支持。更高級的線程支持不會是 C++11 的一部份,但是其最終實現將建立在目前已有的線程支持之上。std::async 提供了一個簡便方法來運行線程,並將線程綁定在 std::future上。用戶可以選擇一個工作是要在多個線程上異步的運行,還是在一個線程上運行並等待其所需要的數據。默認的情況,實現可以根據底層硬件選擇前面兩個選項的其中之一。另外在較簡單的使用場景下,實現也可以利用線程池提供支持。

7.3 元組類型

  元組(tuple)由預先確定數量的多種對象組成.元組可以看作是struct數據成員的泛化.TR1 tuple類型的C++11版本獲益於像可變參數模板這樣的C++11語言特性.TR1版本的元組需要一個由實現定義的包含的類型的最大數目,而且需要大量的宏技巧來實現.相比之下,C++11版本的不需要顯式的實現定義的最大類型數目.儘管編譯器有一個內部的模板實例化的最大遞歸深度,但C++11版的元組不會把它暴露給用戶.
  用可變參數模板,元組類的定義看上去像下面這樣:

複製代碼
template <class ...Types> class tuple;
//下面是定義和使用元組的一個例子:
typedef std::tuple <int, double, long &, const char *> test_tuple;
long lengthy = 12;
test_tuple proof (18, 6.5, lengthy, "Ciao!");
lengthy = std::get<0>(proof);  // 把'lengthy' 賦值爲18.

std::get<3>(proof) = " Beautiful!";  // 修改元組的第四個元素
複製代碼

  可以在定義一個元組時不定義他的內容,不過只有當它的每個元素類型都有默認構造函數時纔可以這樣做.而且,可以將一個元組賦值給另一個元組:如果兩個元組的類型相同,且每種元素類型都有拷貝構造函數;否則,需要右邊的元組的元素可以隱式轉換成左邊元組的對應元素類型或者左邊元組的元素類型有合適的構造函數.

typedef std::tuple <int , double, string       > tuple_1 t1;
typedef std::tuple <char, short , const char * > tuple_2 t2 ('X', 2, "Hola!");
t1 = t2; // Ok, 前兩個元素都是可以轉換的.
            // 第三個可以用'const char *'來構造一個string對象.

  可以對元組類型對象的進行比較運算(當它們擁有同樣數量的元素)。此外,C++11 提供兩個表達式用來檢索元組類型的屬性(僅在編譯期做此檢查)。

std::tuple_size<T>::value //返回元組 T 內的元素個數,
std::tuple_element<I, T>::type //返回元組 T 內的第 I 個元素的類型

 

    7.4 散列表

   在過去,不斷有要求想將散列表(無序關係式容器)引進標準庫。只因爲時間上的限制,散列表纔沒有被標準庫所採納。雖然,散列表在最糟情況下(如果出現許多衝突 (collision) 的話)在性能上比不過平衡樹。但實際運用中,散列表的表現則較好。
因爲標準委員會還看不到有任何機會能將開放尋址法標準化,所以目前衝突僅能通過線性鏈(linear chaining) 的方式來處理。爲避免與第三方庫發展的散列表發生名稱上的衝突,前綴將採用 unordered 而非 hash。
  標準庫將引進四種散列表,其中差別在於以下兩個特性: 是否接受具相同鍵值的項(Equivalent keys),以及是否會將鍵值映射到相對應的數據(Associated values).新的標準庫增加了以下散列表類型:

散列表類型 有無關係值 接受相同鍵值
std::unordered_set
std::unordered_multiset
std::unordered_map
std::unordered_multimap

  這些類完全具備容器類需的條件,同時也提供訪問其中元素的成員函數: insert, erase, begin, end。
  散列表不需要對現有核心語言做擴展(雖然散列表的實現會利用到 C++11 新的語言特性),只會對頭文件 <functional> 做些許擴展,並引入 <unordered_set> 和 <unordered_map> 兩個頭文件。對於其它現有的類型不會有任何修改。同時,散列表也不會依賴標準庫的其它擴展功能。

7.5 正則表達式

  新的標準庫定義了一個新的頭文件<regex>,由一些新的類組成:

  • 正則表達式由模板類std::regex的實例來表示;
  • 模式匹配由的結果模板類std::match_results的實例來表示;

  函數 regex_search 是用來搜索模式的; 若要搜索並替換,則要使用函數 regex_replace,該函數會返回一個新的字符串。算法regex_search 和 regex_replace 接受一個正則表達式(模式)和一個字符串,並將該模式匹配的結果情況存儲在 struct match_results對象中。
下面的例子展示了 match_results 的用法:

複製代碼
const char *reg_esp = "[ ,.\\t\\n;:]";  // 列出分隔符.
// 這也可以通過字符串字面值來完成:
// const char *reg_esp = R"([ ,.\t\n;:])";
std::regex rgx(reg_esp); // 'regex' 是模板類'basic_regex'以'char'爲類型參數特化的類.
std::cmatch match; // 'cmatch'是模板類'match_results'以'const char *'特化的類.
const char *target = "Unseen University - Ankh-Morpork";
 
// 找出'target'中所有以'reg_esp'中的字符分隔的單詞.
if (std::regex_search(target, match, rgx)) {
    // 如果找到了指定的單詞 
    const size_t n = match.size();
    for (size_t a = 0; a < n; a++) {
        std::string str (match[a].first, match[a].second);
        std::cout << str << "\n";
    }
}
複製代碼

  注意雙反斜槓的使用,因爲 C++ 將反斜槓作爲轉義字符使用。但 C++11的原始字符串(raw string)可以用來避免這一問題。庫 <regex> 不需要改動到現有的頭文件,同時也不需要擴展現有的語言特性。

7.6 通用智能指針

  這些指針是由 TR1 智能指針演變而來。注意! 智能指針是類而非一般指針。shared_ptr 是引用計數型(reference-counted) 指針類,其行爲與一般 C++ 指針極爲相似。在 TR1 的實現中,缺少了一些一般指針所擁有的特性,像是別名或是指針運算。C++11增加了這些特性。以下是一個使用 shared_ptr 的例子:

複製代碼
int main( )
{
    std::shared_ptr<double> p_first(new double) ;
    {
        std::shared_ptr<double> p_copy = p_first ;
        *p_copy = 21.2;
    }  // 此時 'p_copy' 會被銷燬,但動態分配的 double 不會被銷燬。
 
    return 0;  // 此時'p_first'會被銷燬,但動態分配的 double也會被銷燬(因爲不再有指針指向它)。
}
複製代碼

  auto_ptr 將會被 C++ 標準所廢棄,取而代之的是unique_ptr。 unique_ptr 提供 auto_ptr 大部份特性,但不包括 auto_ptr 的不安全性和隱性的左值轉移。不像 auto_ptr,unique_ptr 可以存放在 C++11 提出的那些需要移動語義的容器之中。

7.7 可擴展的隨機數生成器

  C 標準庫允許使用rand函數來生成僞隨機數。不過其算法則取決於各程序庫開發者。 C++ 直接從 C 繼承了這部份,但是 C++11 將會提供產生僞亂數的新方法。C++11 的隨機數功能分爲兩部分: 第一,一個隨機數生成引擎,其中包含該生成引擎的狀態,用來產生隨機數。第二,一個分佈,這可以用來決定產生隨機數的範圍,也可以決定以何種分佈方式產生隨機數。隨機數生成對象即是由隨機數生成引擎和分佈所構成。

  不同於 C 標準庫的 rand; 針對產生隨機數的機制,C++11 將會提供三種算法,每一種算法都有其強項和弱項:

樣板類 整數/浮點數 品質 速度 狀態數*
linear_congruential 整數 中等 1
subtract_with_carry 兩者皆可 中等 25
mersenne_twister 整數 624

  C++11 將會提供一些標準分佈: uniform_int_distribution (離散型均勻分佈),bernoulli_distribution (伯努利分佈),geometric_distribution (幾何分佈), poisson_distribution (卜瓦松分佈),binomial_distribution (二項分佈),uniform_real_distribution (離散型均勻分佈), exponential_distribution (指數分佈),normal_distribution (正態分佈) 和 gamma_distribution (伽瑪分佈)。下面的例子展示了一個隨機數生成對象如何由生成引擎和分佈構成的:

std::uniform_int_distribution<int> distribution(0, 99); // 建立分佈,以離散均勻分佈方式在0到99之間產生隨機數
std::mt19937 engine; // 建立隨機數生成引擎
auto generator = std::bind(distribution, engine); // 利用 bind 隨機數生成引擎和分佈組合成一個隨機數生成器
int random = generator();  // 產生隨機數

  7.8 封裝引用

  我們可以通過實例化模板類 reference_wrapper 得到一個封裝引用 (wrapper reference)。封裝引用類似於一般的引用。對於任意對象,我們可以通過模板類 ref 得到一個封裝引用 (至於 constant reference 則可通過 cref 得到)。當模板函數需要形參的引用而非其拷貝時封裝引用就能派上用場了:

複製代碼
// 此函數將得到r的引用,並將r的值加1.
void f (int &r)  { r++; }
 
// 模板函數
template<class F, class P> void g (F f, P t)  { f(t); }
 
int main()
{
    int i = 0 ;
    g (f, i) ;  // 實例化 'g<void (int &r), int>' 
                // 'i' 不會被修改
    std::cout << i << std::endl;  // 輸出 0
 
    g (f, std::ref(i));  // 實例化 'g<void(int &r),reference_wrapper<int>>'
                         // 'i' 會被修改
    std::cout << i << std::endl;  // 輸出 1
}
複製代碼

  這項功能將加入頭文件 <utility> 之中,而非通過擴展語言來得到.

7.9 多態的函數對象包裝器

  函數對象的多態包裝器(又稱多態函數對象包裝器)在語義和語法上和函數指針相似,但不像函數指針那麼狹隘。只要能被調用,且其參數能與包裝器兼容的都能以稱之爲多態函數對象包裝器(函數指針,成員函數指針或仿函數)。

  通過以下例子,我們可以瞭解多態函數對象包裝器的特性:

複製代碼
std::function<int (int, int)> func;  // 利用模板類 'function'
                                     // 建立包裝器
std::plus<int> add;  // 'plus' 被聲明 'template<class T> T plus( T, T ) ;'
                     //  因此 'add' 的類型是 'int add( int x, int y )'
func = &add;  // 可行。'add' 的型參和回返值類型與 'func' 相符
 
int a = func (1, 2);  // 注意: 若包裝器 'func' 沒有引用到任何函數
                      // 會拋出 'std::bad_function_call' 異常
 
std::function<bool (short, short)> func2 ;
if(!func2) { // 因爲還給'func2'賦值,此表達式爲真
 
    bool adjacent(long x, long y);
    func2 = &adjacent ;  // 可行。'adjacent' 的型參和回返值類型可通過類型轉換與'func2'相符
 
    struct Test {
        bool operator()(short x, short y);
    };
    Test car;
    func = std::ref(car);  // 模板類 'std::ref' 返回一個struct 'car' 成員函數 'operator()' 的包裝
}
func = func2;  // 可行。'func2'的型參和回返值類型可通過類型轉換而與'func' 相符
複製代碼

  模板類 function 將定義在頭文件 <functional>,而不須改動語言本身。

7.10 用於元編程的類型特徵

  編寫一個創建或修改其它程序(也可以是程序本身)的程序稱爲元編程.這種行爲可以發生在編譯期,也可以發生在運行期.C++標準委員會決定引入一個庫,允許在編譯期利用模板進行元編程.
以下是一個元編程的例子,基於當前的C++03標準: 模板的遞歸實例化來計算整數的冪

複製代碼
template<int B, int N>
struct Pow {
    // 遞歸調用和重新組合.
    enum{ value = B*Pow<B, N-1>::value };
};
 
template< int B >
struct Pow<B, 0> {
    // ''N == 0'' 是終止條件.
    enum{ value = 1 };
};
int quartic_of_three = Pow<3, 4>::value;
複製代碼

  很多算法都可以操作不同類型的數據;C++的模板支持泛型編程,並且使代碼更加緊湊和有用.然而算法通常都需要知道它正使用的數據的類型.這些信息可以在編譯期利用類型特徵(type trait)獲取到. 類型特徵(type traits)可以標識出一個對象的類別和類或結構體的全部特徵.type traits定義在一個新頭文件<type_traits>中.
在下面的例子中,模板函數elaborate根據給定的數據類型實例化了一個特定的算法(algorithms.do_it)

複製代碼
// 算法一
template< bool B > struct Algorithm {
    template<class T1, class T2> static int do_it (T1 &, T2 &)  { /*...*/ }
};
 
// 算法二
template<> struct Algorithm<true> {
    template<class T1, class T2> static int do_it (T1, T2)  { /*...*/ }
};
 
// 根據給定的類型,會自動實例化正確的算法.
template<class T1, class T2>
int elaborate (T1 A, T2 B)
{
    // 只有當T1是整數,T2是浮點數是實例化算法二
    // 其他情況實例化算法一
    return Algorithm<std::is_integral<T1>::value && std::is_floating_point<T2>::value>::do_it( A, B ) ;
}
複製代碼

  通過定義在<type_transform>頭文件中的類型特徵,可以創建類型轉換操作(static_cast和const_cast對模板來說是不夠的).這種編程技術能產生優雅簡潔的代碼,但是除錯卻是這種技術的弱點:編譯期的錯誤信息難以理解,運行期錯誤則更難.

7.11 用於計算函數對象返回類型的統一方法

要在編譯期確定一個模板函數的返回值類型不是那麼容易的,特別是當返回類型依賴於函數參數的時候.例如:

複製代碼
struct Clear {
    int    operator()(int) const;    // 參數類型
    double operator()(double) const; // 與返回類型相同
};
 
template <class Obj>
class Calculus {
public:
    template<class Arg> Arg operator()(Arg& a) const {
        return member(a);
    }
private:
    Obj member;
};
//以Clear來實例化Calculus模板類(Calculus<Clear>), Calculus類的所有對象都有與Clear類相同的返回類型.但是下面給出的Confused類:
struct Confused {
    double operator()(int) const;     // 參數類型
    int    operator()(double) const;  // 不同於返回類型
};
//試圖實例化Calculus<Confused>會導致Calculus與Confused的返回類型不同.編譯會產生一條從int轉換到double的警告和一條從double轉換到int的警告信息.
//在TR1引入,C++11也接受了模板類std::result_of,它允許我們在所有的聲明中確定和使用函數對象的返回類型.下面的類CalculusVer2用std::result_of對象來獲得函數對象的返回類型:
template< class Obj >
class CalculusVer2 {
public:
    template<class Arg>
    typename std::result_of<Obj(Arg)>::type operator()(Arg& a) const {
        return member(a);
    }
private:
    Obj member;
};
//這種實例化CalculusVer2<Confused>函數對象的方法沒有類型轉換,沒有警告也沒錯誤.
複製代碼

 

  模板 std::result_of 在 TR1 和 C++11 的實現中有一點不同。TR1 的版本允許實現在特殊情況下,可能無法決定一個函數調用其回返值的類別。然而,因爲 C++11支持了decltype,實現被要求在所有情況下,皆能計算出回返值類型。

 

7.12 原本計劃加入但沒有加入C++11的特性  

  預計由 Technical Report 提供支持的:

  • 模塊
  • 十進制類別
  • 數學專用函數

  延後討論的:

  • Concepts (概念 (C++))
  • 更完整或必備的垃圾回收支持
  • Reflection 反射
  • Macro Scopes 宏作用域

7.13 被移除或被廢棄的特性  

  • 順序點 (sequence point),這個術語正被更爲易懂的描述所取代。一個運算可以發生 (is sequenced before) 在另一個運算之前; 又或者兩個運算彼此之間沒有順序關係 (are unsequenced)。
  • export,用法已被去掉,但關鍵字還是被保留了,給將來可能的特性使用
  • exception specifications
  • std::auto_ptr 被 std::unique_ptr 取代。
  • 函數對象的基類(std::unary_function, std::binary_function)、函數指針適配器、類型成員指針適配器以及綁定器 (binder)。

  

  結尾: 衷心希望看到的朋友能給點意見或建議

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章