深刻理解C++11【4】

深刻理解C++11【4】ios

一、基於範圍的 for 循環程序員

  C++98 中須要告訴編譯器循環體界面範圍。如for,或stl 中的for_each:算法

int main() 
{ 
    int arr[ 5] = { 1, 2, 3, 4, 5}; 
    int * p; 
    for (p = arr; p < arr + sizeof( arr)/ sizeof( arr[ 0]); ++ p){ 
        *p *= 2; 
    } 
    
    for (p = arr; p < arr + sizeof( arr)/ sizeof( arr[ 0]); ++ p){ 
        cout << *p << '\t'; 
    } 
} 
View Code
int action1( int & e){ e *= 2; } 
int action2( int & e){ cout << e << '\t'; } 

int main() { 
    int arr[ 5] = { 1, 2, 3, 4, 5}; 
    for_ each( arr, arr + sizeof( arr)/ sizeof( arr[ 0]), action1); 
    for_ each( arr, arr + sizeof( arr)/ sizeof( arr[ 0]), action2); 
}

   由 程序員 來講 明 循環 的 範圍是 多餘 的, 也是 容易 犯錯誤 的。 而 C++ 11 也 引入 了 基於 範圍 的 for 循環, 就能夠 很好 地 解決 了 這個 問題。express

int main() 
{ 
    int arr[ 5] = { 1, 2, 3, 4, 5 }; 
    for (int & e: arr) e *= 2; 
    for (int & e: arr) cout << e << '\t'; 
}

  上棕採用了引用的形式,也能夠不採用引用的形式。編程

for (int e: arr) cout << e << '\t';

  結合以前的 auto,會更簡練。數組

for (auto e: arr) cout << e << '\t';

   下述 代碼 會 報錯, 由於 做爲參數傳遞而來的數組 a 的範圍不能肯定, 所以 也就 不能 使用 基於 範圍 循環 for 循環 對其 進行 迭代 的 操做。app

int func( int a[]) 
{ 
    for (auto e: a) cout << e; 
} 

int main() 
{ 
    int arr[] = {1, 2, 3, 4, 5}; 
    func( arr); 
}

 

二、枚舉:分門別類與數值的名字。less

  覺的常量定義方式:ide

  1)宏。宏 的 弱點 在於 其 定義 的 只是 預處理 階段 的 名字, 若是 代碼 中有 Male 或者 Female 的 字符串, 不管 在什麼 位置 一概 將被 替換。 這 在 有的 時候 會 干擾 到 正常 的 代碼,函數

#define Male 0 
#define Female 1

  2)枚舉。相比於宏,會 獲得 編譯器 的 檢查。且 不會有 干擾 正常 代碼 的 尷尬。

enum { Male, Female };

  3)靜態常量。因爲 是 靜態 常量, 其 名字 做用域 也 被 很好 地 侷限於 文件 內。

const static int Male = 0; 
const static int Female = 1;

 

三、有缺陷的枚舉類型

  C/ C++ 的 enum 有個 很「 奇怪」 的 設定, 就是 具名( 有 名字) 的 enum 類型 的 名字, 以及 enum 的 成員 的 名字 都是 全局 可見 的。下面代碼,Category 中的 General 和 Type 中的 General 都是 全局 的 名字, 所以 編譯會報錯

enum Type { General, Light, Medium, Heavy }; 
enum Category { General, Pistol, MachineGun, Cannon };

  匿名space的 enum仍是會污染全局空間。以下:

namespace T
{ 
    enum Type { General, Light, Medium, Heavy }; 
} 

namespace 
{ 
    enum Category { General = 1, Pistol, MachineGun, Cannon }; 
} 

int main() 
{ 
    T:: Type t = T:: Light; 
    if (t == General) // 忘記 使用 namespace 
        cout << "General Weapon" << endl; 
    return 0; 
}

   C++98 中並不會阻止不一樣enum 間的比較。

enum Type { General, Light, Medium, Heavy }; 
//enum Category { General, Pistol, MachineGun, Cannon }; // 沒法 編譯 經過, 重複 定義 了 General 
enum Category { Pistol, MachineGun, Cannon }; 

int main() 
{ 
    Type type1;
    if (type1 >= Pistol) 
        cout << "It is not a pistol" << endl; 
}

   C++98 中能夠經過封閉來解決上述污染和類型比較的問題。

class Type { 
    public: 
        enum type { general, light, medium, heavy }; 
        type val; 
        public: Type( type t): val( t){} 
        bool operator >= (const Type & t) { return val >= t. val; } 
        static const Type General, Light, Medium, Heavy; 
}; 
const Type Type:: General( Type:: general); 
const Type Type:: Light( Type:: light); 
const Type Type:: Medium( Type:: medium); 
const Type Type:: Heavy( Type:: heavy); 

class Category { 
    public: 
        enum category { pistol, machineGun, cannon }; 
        category val; 
    public: 
        Category( category c): val( c) {} 
        bool operator >= (const Category & c) { return val >= c. val; } 
        static const Category Pistol, MachineGun, Cannon; 
}; 
const Category Category:: Pistol( Category:: pistol); 
const Category Category:: MachineGun( Category:: machineGun);
const Category Category:: Cannon( Category:: cannon); 

struct Killer { 
    Killer( Type t, Category c) : type( t), category( c){} 
    Type type; 
    Category category; 
}; 

int main() { // 使用 類型 包裝 後的 enum 
    Killer notCool( Type:: General, Category:: MachineGun); 
    // ... 
    // ...其餘 不少 代碼... 
    // ... 
    if (notCool. type >= Type:: General) // 能夠 經過 編譯 
        cout << "It is not general" << endl; 
    if (notCool. type >= Category:: Pistol) // 該 句 沒法 編譯 經過 
        cout << "It is not a pistol" << endl; 
    // ... 
        cout << is_ pod< Type>:: value << endl; // 0 
        cout << is_ pod< Category>:: value << endl; // 0 
    return 0; 
}
View Code

   編譯器 會 根據 數據類型 的 不一樣 對 enum 應用 不一樣 的 數據 長度。 在 咱們 對 g++ 的 測試 中, 普通 的 枚舉 使用 了 4 字節 的 內存, 而 當 須要 的 時候, 會 拓展 爲 8 字節。

enum C { C1 = 1, C2 = 2}; 
enum D { D1 = 1, D2 = 2, Dbig = 0xFFFFFFF0U }; 
enum E { E1 = 1, E2 = 2, Ebig = 0xFFFFFFFFFLL}; 
int main() 
{ 
    cout << sizeof( C1) << endl; // 4 
    cout << Dbig << endl; // 編譯器 輸出 不一樣, g++: 4294967280 
    cout << sizeof( D1) << endl; // 4 
    cout << sizeof( Dbig) << endl; // 4 
    cout << Ebig << endl; // 68719476735 
    cout << sizeof( E1) << endl; // 8 
    return 0; 
} 

  不一樣的編譯器,上例中 Dbig 的 輸出 結果 將會 不一樣: 使用 Visual C++ 編譯程序 的 輸出 結果 爲– 16, 而使 用 g++ 來 編譯 輸出 爲 4294967280。 這是 因爲 Visual C++ 老是 使用 無符號 類型 做爲 枚舉 的 底層 實現, 而 g++ 會 根據 枚舉 的 類型 進行 變更 形成 的。

四、強類型枚舉(strong-typed enum) 以及 C++ 11 對 原有枚舉類型的擴展

  聲明 強 類型 枚舉 很是 簡單, 只需 要在 enum 後加 上 關鍵字 class。

enum class Type { General, Light, Medium, Heavy };

  強類型枚舉有如下幾個優點:

  1)強做用域,強類型枚舉成員的名稱不會被輸出到其你做用域空間。

  2)轉換限制,強類型枚舉成員的值不能夠與整形隱式地相互轉換。

  3)能夠 指定 底層 類型。 強 類型 枚舉 默認 的 底層 類型 爲 int, 但也 能夠 顯 式 地 指定 底層 類型。

enum class Type: char { General, Light, Medium, Heavy };

  下面是一個強類型枚舉的例子:

enum class Type { General, Light, Medium, Heavy }; 
enum class Category { General = 1, Pistol, MachineGun, Cannon }; 

int main() { 
    Type t = Type:: Light; 
    
    t = General; // 編譯 失敗, 必須 使用 強 類型 名稱 
    
    if (t == Category:: General) // 編譯 失敗, 必須 使用 Type 中的 General 
        cout << "General Weapon" << endl; 
        
    if (t > Type:: General) // 經過 編譯 
        cout << "Not General Weapon" << endl; 
        
    if (t > 0) // 編譯 失敗, 沒法 轉換 爲 int 類型 
        cout << "Not General Weapon" << endl; 
        
    if ((int) t > 0) // 經過 編譯 
        cout << "Not General Weapon" << endl; 
        
    cout << is_ pod< Type>:: value << endl; // 1 
    cout << is_ pod< Category>:: value << endl; // 1 
    
    return 0; 
} 

   設置較小的基本類型能夠節省空間:

enum class C : char { C1 = 1, C2 = 2}; 
enum class D : unsigned int { D1 = 1, D2 = 2, Dbig = 0xFFFFFFF0U }; 

int main() { 
    cout << sizeof( C:: C1) << endl; // 1 
    cout << (unsigned int) D:: Dbig << endl; // 編譯器 輸出 一致, 4294967280 
    cout << sizeof( D:: D1) << endl; // 4 
    cout << sizeof( D:: Dbig) << endl; // 4 
    return 0; 
} 

   C++11加強了原有枚舉類型:

  1)能夠 跟 強 類型 枚舉 類 同樣, 顯 式 地 由 程序員 來 指定。

enum Type: char { General, Light, Medium, Heavy };

  2)擴展做用域。除了增長到父做用域外,還增長到自身做用域,二者等價。

enum Type { General, Light, Medium, Heavy }; 
Type t1 = General; 
Type t2 = Type:: General;

  

  此外, 咱們 在 聲明 強 類型 枚舉 的 時候, 也能夠 使用 關鍵字 enum struct。 事實上 enum struct 和 enum class 在 語法 上 沒有 任何 區別( enum class 的 成員 沒有 公有 私有 之分,

  有 一點 比較 有趣 的 是 匿名 的 enum class。 因爲 enum class 是 強 類型 做用域 的, 故匿名的 enum class 極可能什麼都作不了

enum class { General, Light, Medium, Heavy } weapon; 

int main() 
{
    weapon = General; // 沒法 編譯 經過 
    bool b = (weapon == weapon:: General); // 沒法 編譯 經過 
    return 0; 
} 

 

五、顯式內存管理。

  常見內存管理問題:

  1)野指針、重複釋放。指向指向內存已被釋放,但指針卻還在被使用。

  2)內存泄露。指針已經丟失,但其指向內存並未被釋放。

 

  C++98中的智能指針是 auto_ptr,不過 auto_ ptr 有 一些 缺點( 拷貝 時 返回 一個 左 值, 不能 調用 delete[] 等), 因此 在 C++ 11 標準中被廢棄了。 C++ 11 標準 中 改用 unique_ ptr、 shared_ ptr 及 weak_ ptr 等 智能 指針 來自 動 回收 堆 分配 的 對象。

  下面是 unique_ptr、shared_ptr 的例子:

#include < memory> 
#include < iostream> 
using namespace std; 
int main() 
{ 
    unique_ ptr< int> up1( new int( 11)); // 沒法 複製 的 unique_ ptr 
    unique_ ptr< int> up2 = up1; // 不能 經過 編譯 
    cout << *up1 << endl; // 11 
    unique_ ptr< int> up3 = move( up1); // 如今 p3 是 數據 惟一 的 unique_ ptr 智能 指針 
    cout << *up3 << endl; // 11 
    cout << *up1 << endl; // 運行時 錯誤 
    up3. reset(); // 顯 式 釋放 內存 
    up1. reset(); // 不會 致使 運行時 錯誤 
    cout << *up3 << endl; // 運行時 錯誤
    
    shared_ ptr< int> sp1( new int( 22)); 
    shared_ ptr< int> sp2 = sp1; 
    cout << *sp1 << endl; // 22 
    cout << *sp2 << endl; // 22 
    sp1. reset(); 
    cout << *sp2 << endl; // 22 
}

   unique_ ptr 則是 一個 刪除 了 拷貝 構造 函數、 保留 了 移動 構造 函數 的 指針 封裝 類型。

  shared_ptr 在實現 上 採用 了 引用 計數, 因此 一旦 一個 shared_ ptr 指針 放棄 了「 全部權」( 失效), 其餘 的 shared_ ptr 對對 象 內存 的 引用 並不 會 受到影響。

  weak_ptr 可 以 指向 shared_ ptr 指針 指向 的 對象 內存, 卻 並不 擁有 該 內存。 而使 用 weak_ ptr 成員 lock, 則 可 返回 其 指向 內存 的 一個 shared_ ptr 對象, 且 在 所指 對象 內存 已經 無效 時, 返回 指針 空 值( nullptr, 請 參見 7. 1 節)。 這 在 驗證 share_ ptr 智能 指針 的 有效性 上 會很 有 做用。以下:

#include < memory> 
#include < iostream> 
using namespace std; 
void Check( weak_ ptr< int> & wp) 
{ 
    shared_ ptr< int> sp = wp. lock(); // 轉換 爲 shared_ ptr< int> 
    
    if (sp != nullptr) 
        cout << "still " << *sp << endl; 
    else 
        cout << "pointer is invalid." << endl; 
} 

int main() { 
    shared_ ptr< int> sp1( new int( 22)); 
    shared_ ptr< int> sp2 = sp1; 
    weak_ ptr< int> wp = sp1; // 指向 shared_ ptr< int> 所指 對象 
    cout << *sp1 << endl; // 22 
    cout << *sp2 << endl; // 22 
    Check( wp); // still 22 
    sp1. reset(); 
    cout << *sp2 << endl; // 22 
    Check( wp); // still 22 
    sp2. reset(); 
    Check( wp); // pointer is invalid
}
    

 

六、垃圾回收的分類。

  垃圾回收的方式能夠分爲兩大類:

  1)基於引用計數。這種 方法 比較 難處理「 環形 引用」 問題, 此外 因爲 計數 帶來 的 額外 開銷 也 並不 小。

  2)基於跟蹤處理。跟蹤 處理 的 垃圾 回收 機制 被 更爲 普遍 地 應用。主要有如下幾種方法:

    a)標記 - 清除(Mark-Sweep)

      這種 方法 的 特色 是 活的 對象 不會 被 移動, 可是 其 存在 會 出現 大量 的 內存 碎片 的 問題。

    b)標記 - 整理(Mark-Compact)

      這個 算法 標記 的 方法 和 標記- 清除 方法 同樣, 可是 標記 完 以後, 再也不 遍歷 全部 對象 清掃 垃圾 了, 而是 將 活的 對象 向「 左」 靠 齊, 這就 解決 了 內存 碎片 的 問題。

    c)標記 - 拷貝(Mark-Copy)

      標記– 整理 算法 的 另外一種 實現。這種 算法 將 堆 空間 分爲 兩個 部分: From 和 To。

 

  C++ 11 標準 也 開始 對 垃圾 回收 作了 必定 的 支持, 雖然 支持 的 程度 還 很是 有限。

 

七、C++ 與垃圾回收。

  由於C++中能夠自由移動指針,因此若是加入垃圾回收,有可能將有用的內存回收,從而致使重大內存問題。

int main()
{ 
    int* p = new int; 
    p += 10; // 移動 指針, 可能 致使 垃圾 回收 器 
    p -= 10; // 回收 原來 指向 的 內存 
    *p = 10; // 再次 使用 本來 相同 的 指針 則 可能 無效 
}

  下例也是同樣,隱藏原始指針,可能致使內存被回收問題。

int main() 
{ 
    int *p = new int; 
    int *q = (int*)( reinterpret_ cast< long long>( p) ^ 2012); // q 隱藏 了 p 
    // 作 一些 其餘 工做, 垃圾 回收 器 可能 已經 回收 了 p 指向 對象 
    q = (int*)( reinterpret_ cast< long long>( q) ^ 2012); // 這裏 的 q == p 
    *q = 10; 
}

 

八、C++11 與最小垃圾回收支持。

   截至2013年, 幾乎沒有 編譯器 實現 了 最小 垃圾 回收 支持, 甚至 連 get_ pointer_ safety 這個 函數 接口 都 還沒 實現。

  declare_ reachable() 顯 式 地 通知 垃圾 回收 器 某一個 對象 應被 認爲 可達 的, 即便 它的 全部 指針 都對 回收 器 不 可見。 undeclare_ reachable() 則 能夠 取消 這種 可達 聲明。

#include < memory> 
using namespace std; 
int main() 
{ 
    int *p = new int; 
    declare_ reachable( p); // 在 p 被 隱藏 以前 聲明 爲 可達 的 
    int *q = (int*)(( long long) p ^ 2012); // 解除 可達 聲明 
    q = undeclare_ reachable< int>(( int*)(( long long) q ^ 2012)); 
    *q = 10; 
}

   有的 時候 程序員 會 選擇 在 一大 片 連續 的 堆 內存 上 進行 指針式 操做, 爲了 讓 垃圾 回收 器 不關心 該 區域, 也能夠 使用 declare_ no_ pointers 及 undeclare_ no_ pointers 函數 來 告訴 垃圾 回收 器 該 內存 區域 不存在 有效 的 指針。

void declare_ no_ pointers( char *p, size_ t n) noexcept; 
void undeclare_ no_ pointers( char *p, size_ t n) noexcept;

 

九、垃圾回收的兼容性。

  C++ 11 標準 中 對 指針 的 垃圾 回收 支持 僅限 於 系統 提供 的 new 操做 符 分配 的 內存, 而 malloc 分配 的 內存 則 會被 認爲 老是 可達 的, 即 不管 什麼時候 垃圾 回收 器 都不 予 回收。 所以 使用 malloc 等 的 較老 代碼 的 堆 內存 仍是 必須 由 程序員 本身 控制。

 

十、運行時常量性與編譯時常量性。

  const用於保證運行期常量性,但有時候,咱們須要的倒是編譯時的常量性,這是const關鍵字沒法保證的。

const int GetConst() { return 1; } 

void Constless( int cond) 
{ 
    int arr[ GetConst()] = {0}; // 沒法 經過 編譯 
    enum { e1 = GetConst(), e2 }; // 沒法 經過 編譯 
    switch (cond) { 
        case GetConst(): // 沒法 經過 編譯 
            break; 
        default: 
            break; 
    } 
}

   上述代碼,咱們發現, 不管 將 GetConst 的 結果 用於 須要 初始化 數組 Arr 的 聲明 中, 仍是 用於 匿名 枚舉 中, 或 用於 switch- case 的 case 表達式 中, 編譯器 都會 報告 錯誤。

  發生 這樣 錯誤 的 緣由 如 咱們 上面 提到 的 同樣, 這些 語句 都 需 要的 是 編譯 時期 的 常 量值。 而 const 修飾 的函數 返回 值, 只 保證 了 在 運行時 期內 其 值 是 不能夠 被 更改 的。 這是 兩個 徹底 不一樣 的 概念。

enum BitSet { 
    V0 = 1 << 0, 
    V1 = 1 << 1, 
    V2 = 1 << 2, 
    VMAX = 1 << 3 
}; 

// 重定 義 操做 符"|", 以 保證 返回 的 BitSet 值 不超過 枚舉 的 最大值 
const BitSet operator|( BitSet x, BitSet y) { 
    return static_ cast< BitSet>((( int) x | y) & (VMAX - 1)); 
} 

template < int i = V0 | V1> // 沒法 經過 編譯 
void LikeConst(){} 

  上述代碼,咱們將 V0| V1 做爲 非 類型 模板 函數 的 默認 模板 參數, 則 會 致使 編譯 錯誤。 這 一樣 是由 需 要的 是 編譯 時 常量 所 致使 的。 

   宏能夠解決上述問題,但這種 簡單 粗暴 的 作法 即便 有效, 也 會把 C++ 拉回「 石器 時代」。 C++ 11 中 對 編譯 時期 常量 的 回答 是 constexpr, 即 常量表達式( constant expression)

constexpr int GetConst() { return 1; }

   在 函數 表達式 前 加上 constexpr 關鍵字 便可。 有了 常量 表達式 這樣 的 聲明, 編譯器 就可 以在 編譯 時期 對 GetConst 表達式 進行 值 計算( evaluation), 從而 將其 視爲 一個 編譯 時期 的 常量。

  這樣一來 上述代碼 數組 Arr、 匿名 枚舉 的 初始化 以及 switch- case 的 case 表達式 經過 編譯 都 再也不 是 問題。

 

十一、常量表達式函數。

   並不是 全部 的 函數 都有 資格 成爲 常量 表達式 函數。 事實上, 常量 表達式 函數 的 要求 很是 嚴格, 總結 起來, 大概 有 如下 幾點:

  1)函數體只有單一的return返回語句。例如,下面的的寫會編譯錯誤:

constexpr int data() { const int i = 1; return i; }

  不過 一些 不會 產生 實際 代碼 的 語句 在 常量 表達式 函數 中 使用 下, 倒 不會 致使 編譯器 的「 抱怨」。

constexpr int f( int x)
{ 
    static_ assert( 0 == 0, "assert fail."); 
    return x; 
}

  上面例子 可以 經過 編譯。 而 其餘 的, 好比 using 指令、 typedef 等 也 一般 不會 形成 問題。

  2)函數必須返回值(不能是void函數)。例如,下面的寫法,就不能經過:

constexpr void f(){}

  3)在使用前必須已有定義。

constexpr int f(); 
int a = f(); 
const int b = f(); 
constexpr int c = f(); // 沒法 經過 編譯 
constexpr int f() { return 1; } 
constexpr int d = f(); 

  constexpr 不算重載,會致使編譯錯誤。

constexpr int f(); 
int f();

  4)return 返回 語句 表達式 中 不能 使用 非 常量 表達式 的 函數、 全局 數據, 且 必須 是一 個 常量 表達式。例如如下constexpr不能經過編譯。

const int e(){ return 1;} 
constexpr int g(){ return e(); } // 編譯錯誤

int g = 3; 
constexpr int h() { return g; } // 編譯錯誤

  另外,constexpr中賦值語句也是不容許的。如下會編譯錯誤。

constexpr int k( int x) { return x = 1; }

 

十二、常量表達式的值。

  C++ 11 標準 中, constexpr 關鍵字 是 不能 用於 修飾 自定義 類型 的 定義 的。如下代碼,沒法經過編譯。

constexpr struct MyType {int i; } 
constexpr MyType mt = {0};

  正確地 作法 是, 定義 自定義 常量構造函數( constent- expression constructor)。 

struct MyType 
{ 
    constexpr MyType( int x): i( x){} 
    int i; 
}; 

constexpr MyType mt = {0};

  常量 表達式 的 構造 函數 也有 使 用上 的 約束, 主 要的 有 如下 兩點:

  1)函數體必須爲空。

  2)初始化列表只能由常量表達式來賦值。例如,如下常量構造函數是錯誤的。

int f();
struct MyType {int i; constexpr MyType():i(f()){}};

   常量構造函數、成員函數的區別:

struct Date
{
    constexpr Date(int y, int m, int d):
                    year(y), month(m),day(d){}

    constexpr int GetYear() { return year; }
    constexpr int GetMonth() { return month; }
    constexpr int GetDay() { return day; }
    
private:
    int year;
    int month;
    int day;
}

constexpr Date PRCfound {1949,10,1};
constexpr int foundmont = PRCfound.GetMonth();
int main()
{
    count << foundmonth << endl; //  10
}
View Code

  C++11中,不容許常量表達式做用於 virtual 成員函數。

 

1三、常量表達式的其餘應用

  常量表達式能夠用於模板函數。不過因爲模板中類型的不肯定性,因此模板函數是否會被實例化爲一個可以知足編譯時常量性的版本一般也是未知的。  

  C++11規定,當聲明爲常量表達式的模板函數後,而某個該模板函數的實例化結果不知足常量表達式的需求的話,constexpr會被自動忽略。該實例化後的函數將成爲一個普通函數。

struct NotLiteral{
    NotLiteral(){i=5;}
    int i;
};
NotLiteral nl;
template<typename T> constexpr T ConstExp(T t){
    return t;
}
void g(){
    NotLiteral nl;
    NotLiteral nl1 = ConstExp(nl);
    constexpr NotLiteral nl2 = ConstExp(nl); // 沒法經過編譯
    constexpr int a = ConstExp(1);
}

    上述代碼,NotLiteral不是一個定義了常量表達式構造函數的類型,ConstExp一旦以 NotLiteral爲參數的話,那麼其 constexpr關鍵字將被忽略,因此 ConstExp<NotLiteral>函數不是一個常量表達式函數。

  C++11 標準對常量表達式支持至少 512層遞歸。

constexpr int Fibonacci(int n){
    return (n==1)?1:((n--2)?1:Fibonacci(n-1)+Fibonacci(n-2));
}

int main(){
    int fib[] = {
        Fibonacci(11), Fibonacci(12),
        Fibonacci(13), Fibonacci(14),
        Fibonacci(15), Fibonacci(16)
    };
    
    for (int i : fib)
        cout<<i<<endl;
}

   在C++98中,上述常量表達式函數遞歸能夠經過模板元編程來完成。

template<long num>
struct Fibonacci{
    static const long val = Fibonacci<num-1>::val + Fibonacci<num-2>::val;
}
template<> struct Fibonacci<2> { static const long val =1; }
template<> struct Fibonacci<1> { static const long val =1; }
template<> struct Fibonacci<0> { static const long val =0; }

 

1四、變長函數和變長的模板參數

   C++98 中使用 va_start、va_arg、 va_end 來實現變長函數參數功能。

double SumOfFloat(int count, ...){
    va_list ap;
    ouble sum = 0;
    va_start(ap, count);
    for(int i = 0; i < count; i++)
        sum++ va_arg(ap, double);
    va_end(ap);
    
    return sum();
}

int main(){
    return printf("%f\n", SumOfFloat(3,1.2f,3.4,5.6));
}

   C++98要求函數模板始終具備數目肯定的模板參數。

1五、 變長模板:模板參數包和函數參數包

  標識符前加三個點,表示該參數是變長的。Elements被稱爲模板參數包(template parameter pack)

template <typename... Elements>
class tuple;

  模板參數包也能夠是非類型的,例如:

template<int... A> class NonTypeVariadicTemplate{};
NonTypeVariadicTemplate<1,0,2> ntvt;

  解包(unpack)在尾部添加三個點。下面這個叫包擴展(pack expansion)表達式

template<typename... A> class Template : private B<A...>{};

   上面只是展開了 A...,但沒有使用展開後的內容。Tunple 的實現中,展現了一種使用包括展的方法,數學概括法(遞歸)。這與模板元編程,constexpr很是類似。

// 變長模板的聲明
template <typename... Elements> class tuple;

// 遞歸的偏特化的定義
template <typename Head, typename... Tail>
class tuple<Head, Tail...>:private tuple<Tail...>{
    Head head;
};

// 邊界條件
template<> class tuple<>{};

  下面是一個使用非類型模板的例子。

// 聲明
template <long... nums> struct Multiply;

// 偏特化
template <long first, long... last>
struct Multiply<first, last...>{
    static const long val = first*Multiply<last...>::val;
};

// 邊界條件
template<>
struct Multiply<>{
    static const long val = 1;
};

   C++11中,還能夠聲明變長模板函數。變長模板函數的參數能夠聲明成爲函數參數包(function parameter pack)

template<typename... T> void f(T... args);

   C++11中,標準要求函數參數包必須惟一,且是函數最後一個參數(模板參數包沒有這樣的要求)。

  有了模板參數包、函數參數包,就能夠實現C中變長函數的功能了。

void Printf(const char* s)
{
    while(*s){
        if(*s=='%'&&*++s!='%')
            throw runtime_error("invalid format string");
        cout<<*s++;
    }
}

template<typename T, typename... Args>
void Printf(const char*s, T value, Args... args)
{
    while(*s){
        if(*s=='%'&&*++s!='%'){
            cout<<value;
            return Printf(++s, args...);
        }
        cout<<*s++;
    }
    throw runtime_error("extra arguments provided to Printf");
}

int main(){
    Printf("hello %s\n", string("world")); // hello world
}

 

1六、變長模板:進階

  注意到前面的程序,包擴展形式以下:

template<typename... A> class T: private B<A...> {};
// 上述模板 T<X,Y> 將會解包爲:
class T<X,Y>:private B<X,Y> {};


template<typename... A> class T : private B<A>...{};
// 上述模板 T<X,Y> 將會解包爲:
class T<X,Y>:private B<X>, private B<Y>{};

   也即,解包發生成繼承基類模板時。B<A...> pack expasion 發生在<>內。而 B<A>... pack expasion 發生在 <>外。

  類型的解包也能夠發生在使用模板函數時。以下:

template<truename... T> void DummyWrapper(T... t){}
template<typename T> T pr(T t){
    cout << t;
    return t;
}

template<typename... A>
void VTPrint(A... a){
    DummyWrapper(pr(a)...); // 包擴展爲 pr(1), pr(", "), ..., pr(", abc\n")
}

int main()
{
    VTPrint(1,", ", 1.2, ", abc\n");
}

   C++11中引入 了新操做符「sizeof...」(sizeof後面加上了3個小點),其做用是計算參數包中的參數個數。下面是一個示例:

template<class...A> void Print(A... arg){
    assert(false); // 非6參數偏特化都會默認assert(false)
}

// 特化6參數的版本
void Print(int a1, int a2, int a3, int a4, int a5, int a6){
    cout<<a1<<", "<<a2<<", "<<a3<<", "
        <<a4<<", "<<a5<<", "<<a6<<endl;
}

template<class...A> int Vaargs(A... args){
    int size = sizeof...(A); // 計算變長包的長度
    switch(size){
        case 0: Print(99, 99, 99, 99, 99, 99);
            break;
        case 1: Print(99,99,args...,99,99,99);
            break;
        case 2: Print(99,99,args...,99,99);
            break;
        case 3: Print(args...,99,99,99);
            break;
        case 4: Print(99,args...,99);
            break;
        case 5: Print(99,args...);
            break;
        case 6: Print(args...);
            break;
        case default: 
            Print(0,0,0,0,0,0);
    }
}

int main(void)
{
    Vaargs();    // 99,99,99,99,99,99
    Vaargs(1);    // 99,99,1,99,99,99
    Vaargs(1,2);    // 99,99,1,2,99,99
    Vaargs(1,2,3);    // 1,2,3,99,99,99
    Vaargs(1,2,3,4);    // 99,1,2,3,4,99
    Vaargs(1,2,3,4,5);    // 99,1,2,3,4,5
    Vaargs(1,2,3,4,5,6);    // 99,1,2,3,4,5,6
    Vaargs(1,2,3,4,5,6,7);    // 0,0,0,0,0,0
    return 0;
}

   使用模板作變長模板參數包:

template<typename I, template<typename> class... B> struct Container{};
template<typename I, template<typename> class A, template<typename> class... B>
struct Container<I, A,B...>{
    A<I> a;
    Container<I,B...> b;
}
template<truename I> struct Container<I>{};

   下述代碼會編譯錯誤,模板參數包不是變長模板類的最後一個參數。

template<class...A, class...B> struct Container{};
template<class...A, class B> struct Container{};
相關文章
相關標籤/搜索