1. 程式人生 > >第18章 探討C++新標準

第18章 探討C++新標準

本章首先複習前面介紹過的C++11功能,然後介紹如下主題:

  • 移動語義和右值引用
  • Lambda表示式
  • 包裝器模板function
  • 可變引數模板

18.1 複習前面介紹過的C++11功能 
18.1.1 新型別 
18.1.2 統一的初始化

  • C++11擴大了用大括號括起的列表(初始化列表)的適用範圍,使其可用於所有內建型別和使用者定義的型別(即類物件).使用初始化列表時,可新增登好(=),也可不新增.
  • 1.縮窄 
    • 初始化列表語法可防止縮窄,即禁止將數值賦給無法儲存它的數值變數.
  • 2.std::initializer_list 
    • C++11提供了模板類std::initializer_list,可將其用作建構函式的引數.

18.1.3 宣告

  • C++11提供了多種簡化宣告的功能,尤其在使用模板時.
  • 1.auto 
    • 以前,關鍵字auto是一個儲存型別說明符,C++11將其用於實現自動型別推斷.
    • 這要求進行顯式初始化,讓編譯器能夠將變數的型別設定為初始值的型別.
    • 關鍵字auto還可簡化模板宣告.
  • 2.decltype 
    • 關鍵字decltype將變數的型別宣告為表示式指定的型別.
    • decltype的工作原理比auto複雜,根據使用的表示式,指定的型別可以為引用和const.
  • 3.返回型別後置 
    • 新增的一種函式宣告語法:在函式名和引數列表後面(而不是前面)指定返回型別;
  • 4.模板別名:using = 
    • typedef和using=的差別在於,新語法也可用於模板部分具體化,單typedef不能;
  • 5.nullptr 
    • 空指標是不會指向有效資料的指標.以前,C++在原始碼中使用0表示這種指標,但內部表示可能不同.
    • 它是指標型別,不能轉換為整型型別.
    • 為向後相容C++11仍允許使用0來表示空指標,因此表示式nullptr==0為true,但使用nullptr而不是0提供了更高的型別安全.

18.1.4 智慧指標

  • 基於程式設計師的程式設計體驗和BOOST庫提供的解決方案,C++11摒棄了auto_ptr,並新增了三種智慧指標:unique_ptr,shared_ptr和weak_ptr.
  • 所有新增的智慧指標都能與STL容器和移動語義協同工作.

18.1.5 異常規範方面的修改

  • 與auto_ptr一樣,C++程式設計社群的集體經驗表明,異常規範的效果沒有預期的好.因此,C++11摒棄了異常規範.然而,標準委員會認為,指出函式不會引發異常有一定的價值,他們為此添加了關鍵字noexcept;

18.1.6 作用域內列舉

  • 傳統的C++列舉提供了一種建立名稱常量的方式,但其型別檢查相當低階.另外,列舉名的作用域為列舉定義所屬的作用域,這意味著如果在同一個作用域內定義兩個列舉,他們的列舉成員不能同名.最後,列舉可能不是可完全移植的,因為不同的實現可能選擇不同的底層型別.
  • 為解決以上問題,C++11新增了一種列舉,這種列舉使用class或struct定義;

18.1.7 對類的修改

  • 1.顯式轉換運算子 
    • C++引入了關鍵字explicit,以禁止單引數建構函式導致的自動轉換.
    • C++11擴充套件了explicit的這種用法,使得可對轉換函式做類似的處理.
  • 2.類內成員初始化 
    • 這樣做,可避免在建構函式中編寫重複的程式碼,從而降低了程式設計師的工作量,厭倦情緒和出錯的機會.
    • 如果建構函式在成員初始化列表中提供了相應的值,這些預設值將被覆蓋.

18.1.8 模板和STL方面的修改

  1. 基於範圍的for迴圈 
    • 如果要在迴圈中修改陣列或容器的每個元素,可使用引用型別.
  2. 新的STL容器 
    • C++11新增了STL容器forward_list,unordered_map,unordered_multimap,unordered_set和unordered_multiset.
    • C++11還新增了模板array.要例項化這種模板,可指定元素型別和固定的元素數.
  3. 新的STL方法 
    • C++11新增了STL方法cbegin()和cend().這些新方法將元素視為const.
    • 與此類似,crbegin()和crend()是rbegin()和rend()的const版本.
  4. valarray升級 
    • 模板valarray獨立於STL開發的,其最初的設計導致無法將基於範圍的STL演算法用於valarray物件.C++11天假了兩個函式(begin()和end()),它們都接收valarray作為引數,並返回迭代器,這些迭代器分別指向valarray物件的第一個元素和最後一個元素後面.
    • 這讓你能夠將基於範圍的STL演算法用於valarray.
  5. 摒棄export 
    • 仍保留了關鍵字export,供以後使用
  6. 尖括號 
    • 為避免與運算子>>混淆,C++要求在宣告巢狀模板時使用空格將尖括號分開,C++11不再這樣要求.

18.1.9 右值引用

  • C++11新增了右值引用,這是使用&&表示的.
  • 右值引用可關聯到右值,即可出現在賦值表示式右邊,但不能對其應用地址運算子的值.
  • 程式清單18.1 rvref.cpp

18.2 移動語義和右值引用 
18.2.1 為何需要移動語義

  • 實際檔案還留在原來的地方,而只修改記錄.這種方法被稱為移動語義.有點悖論的是,移動語義實際上避免了移動原始資料,而只是修改了記錄.
  • 要實現移動語義,需要採取某種方式,讓編譯器知道什麼時候需要複製,什麼時候不需要.這就是右值引用發揮作用的地方.

18.2.2 一個移動示例

  • 程式清單18.2 useless.cpp
// useless.cpp -- an otherwise useless class with move semantics
#include <iostream>
using namespace std;
// interface
class Useless
{
private:
    int n;          // number of elements
    char * pc;      // pointer to data
    static int ct;  // number of objects
    void ShowObject() const;
public:
    Useless();
    explicit Useless(int k);
    Useless(int k, char ch);
    Useless(const Useless & f); // regular copy constructor
    Useless(Useless && f);      // move constructor
    ~Useless();
    Useless operator+(const Useless & f)const;
// need operator=() in copy and move versions
    void ShowData() const;
};
// implementation
int Useless::ct = 0;
Useless::Useless()
{
    ++ct;
    n = 0;
    pc = nullptr;
    cout << "default constructor called; number of objects: " << ct << endl;
    ShowObject();
}
Useless::Useless(int k) : n(k)
{
    ++ct; 
    cout << "int constructor called; number of objects: " << ct << endl;
    pc = new char[n];
    ShowObject();
}
Useless::Useless(int k, char ch) : n(k)
{
    ++ct;
    cout << "int, char constructor called; number of objects: " << ct << endl;
    pc = new char[n];
    for (int i = 0; i < n; i++)
        pc[i] = ch;
    ShowObject();
}
Useless::Useless(const Useless & f): n(f.n) 
{
    ++ct;
    cout << "copy const called; number of objects: " << ct << endl;
    pc = new char[n];
    for (int i = 0; i < n; i++)
        pc[i] = f.pc[i];
    ShowObject();
}
Useless::Useless(Useless && f): n(f.n) 
{
    ++ct;
    cout << "move constructor called; number of objects: " << ct << endl;
    pc = f.pc;       // steal address
    f.pc = nullptr;  // give old object nothing in return
    f.n = 0;
    ShowObject();
}
Useless::~Useless()
{
    cout << "destructor called; objects left: " << --ct << endl;
    cout << "deleted object:\n";
    ShowObject();
    delete [] pc;
}
Useless Useless::operator+(const Useless & f)const
{
    cout << "Entering operator+()\n";
    Useless temp = Useless(n + f.n);
    for (int i = 0; i < n; i++)
        temp.pc[i] = pc[i];
    for (int i = n; i < temp.n; i++)
        temp.pc[i] = f.pc[i - n];
    cout << "temp object:\n";
    cout << "Leaving operator+()\n";
    return temp;
}
void Useless::ShowObject() const
{ 
    cout << "Number of elements: " << n;
    cout << " Data address: " << (void *) pc << endl;
}
void Useless::ShowData() const
{
    if (n == 0)
        cout << "(object empty)";
    else
        for (int i = 0; i < n; i++)
            cout << pc[i];
    cout << endl;
}
// application
int main()
{
    {
        Useless one(10, 'x');
        Useless two = one;          // calls copy constructor
        Useless three(20, 'o');
        Useless four(one + three);  // calls operator+(), move constructor
        cout << "object one: ";
        one.ShowData();
        cout << "object two: ";
        two.ShowData();
        cout << "object three: ";
        three.ShowData();
        cout << "object four: ";
        four.ShowData();
    }
    // cin.get();
}
  • 其中最重要的是複製建構函式和移動建構函式的定義.
  • 注意到沒有呼叫移動建構函式,且只建立了4個物件.建立物件four時,該編譯器沒有呼叫任何建構函式;相反,它推斷出物件four是operator+()所做工作的受益人,因此將operator+()建立的物件轉到four的名下.一般而言,編譯器完全可以進行優化,只要結構與未優化時相同.即使省略該程式中的移動建構函式,並使用g++進行編譯,結構也將相同.

18.2.3 移動建構函式解析

  • 雖然使用右值引用可支援移動語義,但這並不會神奇地發生.要讓移動語義發生,需要兩個步驟. 
    • 首先,右值引用讓編譯器知道何時可使用移動語義.
    • 第二步,編寫移動建構函式,使其提供所需的行為.
  • 總之,通過提供一個使用左值引用的建構函式和一個使用右值引用的建構函式,將初始化分成了量足.使用左值物件初始化物件時,將使用複製建構函式,而使用右值物件初始化物件時,將使用移動建構函式.程式設計師可根據需要賦予這些建構函式不同的行為.

18.2.4 賦值

  • 適用於建構函式的移動語義考慮也適用於賦值運算子.
  • 與移動建構函式一樣,移動賦值運算子的引數也不能是const引用,因為這個方法修改了源物件.

18.2.5 強制移動

  • 可使用運算子static_cast<>將物件的型別強制轉換為XXX &&,但C++11提供了一種更簡單的方式—使用標頭檔案utility中宣告的函式std::move().
  • 程式清單18.3 stdmove.cpp
  • 需要知道的是,函式std::move()並非一定會導致移動操作.
  • 對大多數程式設計師來說,右值引用帶來的主要好處並非是讓他們能夠編寫使用右值引用的程式碼,而是能夠使用利用右值引用實現移動語義的庫程式碼.

18.3 新的類功能 
18.3.1 特殊的成員函式

  • 在原有4個特殊成員函式(預設建構函式,賦值建構函式,複製賦值運算子和解構函式)的基礎上,C++11新增了兩個:移動建構函式和移動賦值運算子.

18.3.2 預設的方法和禁用的方法

  • 使用關鍵字default顯式地宣告這些方法得預設版本.
  • 關鍵字delete可用於禁止編譯器使用特定方法.
  • 關鍵字default只能用於6個特殊成員函式,但delete可用於任何成員函式.delete的一種可能用法是禁止特定的轉換.

18.3.3 委託建構函式

  • C++11允許在一個建構函式的定義中使用另一個建構函式.這被稱為委託.

18.3.4 整合建構函式

  • C++11提供了一種讓派生類能夠整合基類建構函式的機制.
  • 這讓派生類整合基類的所有建構函式(預設建構函式,複製建構函式和移動建構函式除外),但不會使用與派生類建構函式的特徵標匹配的建構函式.

18.3.5 管理虛方法:override和final

  • 說明符override和final並非關鍵字,而是具有特殊含義的識別符號.

18.4 Lambda函式 
18.4.1 比較函式指標,函式符和Lambda函式

  • 名稱lambda來自lambda calculus(λ演算)—一種定義和應用函式的數學系統.這個系統讓您能夠使用匿名函式—即無需給函式命名.
  • 程式清單18.4 lambda0.cpp

18.4.2 為何使用lambda

  • 距離,簡潔,效率和功能
  • 函式指標方法組織了內聯,因為編譯器傳統上不會內聯其他地址被獲取的函式,因為函式地址的概念意味著非行內函數.而函式符和lambda通常不會阻止內聯.
  • lambda可訪問作用域內的任何動態變數.
  • 程式清單18.5 lambda1.cpp
// lambda1.cpp -- use captured variables
#include <iostream>
#include <vector>
#include <algorithm>
#include <cmath>
#include <ctime>
const long Size = 390000L;
int main()
{
    using std::cout;
    std::vector<int> numbers(Size);
    std::srand(std::time(0));
    std::generate(numbers.begin(), numbers.end(), std::rand);
    cout << "Sample size = " << Size << '\n';
// using lambdas
    int count3 = std::count_if(numbers.begin(), numbers.end(), 
              [](int x){return x % 3 == 0;});
    cout << "Count of numbers divisible by 3: " << count3 << '\n';
    int count13 = 0;
    std::for_each(numbers.begin(), numbers.end(),
         [&count13](int x){count13 += x % 13 == 0;});
    cout << "Count of numbers divisible by 13: " << count13 << '\n';
// using a single lambda
    count3 = count13 = 0;
    std::for_each(numbers.begin(), numbers.end(),
         [&](int x){count3 += x % 3 == 0; count13 += x % 13 == 0;});
    cout << "Count of numbers divisible by 3: " << count3 << '\n';
    cout << "Count of numbers divisible by 13: " << count13 << '\n';
    // std::cin.get();
    return 0;
}
  • 該程式使用的兩種方法(兩個獨立的lambda和單個lambda)的結果相同.
  • 在C++中引入lambda的主要目的是,能夠將類似於函式的表示式用作接收函式指標或函式符的函式的引數.因此,典型的lambda是測試表達式或比較表示式,可編寫為一條返回語句.這使得lambda簡潔而易於理解,且可自動推斷返回型別.

18.5 包裝器 
18.5.1 包裝器function及模板的低效性

  • 程式清單18.6 somedefs.h
  • 程式清單18.7 callable.cpp

18.5.2 修復問題

  • 程式清單18.8 wrapped.cpp

18.5.3 其他方式

18.6 可變引數模板

  • 要建立可變引數模板,需要理解幾個要點: 
    • 模板引數包(parameter pack);
    • 函式引數包;
    • 展開unpack引數包
    • 遞迴

18.6.1 模板和函式引數包

  • C++提供了一個用省略號表示的元運算子meta-operator,能夠宣告表示模板引數包的識別符號,模板引數包基本上是一個型別列表.同樣,它還讓您能夠宣告表示函式引數包的識別符號,而函式引數包基本上是一個值列表.

18.6.2 展開引數包 
18.6.3 在可變引數模板函式中使用遞迴

  • 程式清單18.9 variadic1.cpp
  • 程式清單18.10 variadic2.cpp
// variadic2.cpp
#include <iostream>
#include <string>
// definition for 0 parameters
void show_list() {}
// definition for 1 parameter
template<typename T>
void show_list(const T& value)
{
    std::cout << value << '\n';
}
// definition for 2 or more parameters
template<typename T, typename... Args>
void show_list(const T& value, const Args&... args)
{
    std::cout << value << ", ";
    show_list(args...); 
}
int main()
{
    int n = 14;
    double x = 2.71828;
    std::string mr = "Mr. String objects!";
    show_list(n, x);
    show_list(x*x, '!', 7, mr);
    return 0;
}

18.7 C++11新增的其他功能 
18.7.1 並行程式設計

  • 為解決並行性問題,C++定義了一個支援執行緒化執行的記憶體模型,添加了關鍵字thread_local,提供了相關的庫檔案.關鍵字thread_local將變數宣告為靜態儲存,其持續性與特定執行緒相關;即定義這種變數的執行緒過期時,變數也將過期.
  • 庫支援由原子操作atomic operation庫和執行緒支援庫組成,其中原子操作庫提供了標頭檔案atomic,而執行緒支援庫提供了標頭檔案thread,mutex,condition,variable和future.

18.7.2 新增的庫 
18.7.3 低階程式設計

  • 低階程式設計中的”低階”指的是抽象程度,而不是程式設計質量.
  • 變化之一是放鬆了POD(Plain Old Data)的要求.
  • 另一項修改時,允許共用體的成員有建構函式和解構函式,這讓共用體更靈活.
  • C++11解決了記憶體對其問題.要獲悉有關型別或物件的對其要求,可使用運算子alignof().要控制對其方式,可使用說明符alignas.
  • constexpr機制讓編譯器能夠在編譯階段計算結果為常量的表示式,讓const變數可儲存在制度記憶體中,這對嵌入式程式設計來說很有用(在執行階段初始化的變數儲存在隨機訪問記憶體中).

18.7.4 雜項

18.8 語言變化 
18.8.1 Boost專案

  • 該計劃的基本理念是,建立一個充當開放論壇的網站,讓人釋出免費的C++庫.這個專案提供有關許可和程式設計實踐的指南,並要求對提議的庫進行同行審閱.

18.8.2 TR1

  • Technical Report 1

18.8.3 使用Boost

  • 程式清單18.11 lexcast.cpp

18.9 接下來的任務

  • OOP有助於開發大型專案,並提高其可靠性.OOP方法的基本活動之一是發明能夠表示正在模擬的情形(被稱為問題域(problem domain))的類.

18.10 總結

  • 無論對新手還是專家來說,新標準都改善了C++的可用性和可靠性.

18.11 複習題 
18.12 程式設計練習

本章原始碼下載地址