摘要訊息 : 想玩玩 C++ 的黑魔法嗎? 來看看這篇文章認識樣板超編程吧!

TIP : 閱讀這個系列的文章之前, 你需要非常熟悉 C++ template. 這個系列的文章將以 C++ 11 為基礎

在 C++ 被全世界熟知之前, 它還只是一個 C with Class. 當時, 還沒有引入樣板. 也就是說, 當時的 C++ 僅僅是一個包含了過程式的語言以及一個物件導向式的語言. 後來, C++ 引入了樣板, 一開始只是給 C++ 帶來了新的一個程式設計範式, 稱它為泛型, 也就是 C++ 又是一個泛型式的語言. 在 C++ 引入樣板之後, 人們發現 template 的威力遠遠超過了人們的想像, 人們意外發現 template 是圖靈完全的. 也就是說, 將樣板作為一門程式設計語言式人們偶然發現的. 因此, C++ 又多了一門新的程式設計範式 - 超編程式. 然而, 在 C++ 中, 超編程一直和 template 相結合, 所以又稱它為樣板超編程 (Template Meta-Programming, 簡稱 TMP). 又由於 TMP 與一些函數式程式設計非常相似, 所以也將 TMP 歸入到函數式程式設計中. 在 C++ 11 引入 lambda 表達式以及更加強大的 <functional> 標頭檔之後, C++ 的函數式程式設計又得到了加強

接下來所有此系列的文章中的 Template Meta-Programming 和樣板超編程都會被縮寫為 TMP

因此, 在 C++ 中存在五種程式設計範式 :

  • 過程式程式設計
  • 物件導向式程式設計
  • 泛型程式設計
  • 函數式程式設計
  • 超編程式程式設計 (樣板超編程)

除此之外, 你還需要認識到, template 其實是可以算作一門獨立的程式設計語言, 也就是 C++ 的子語言. 也就是說, template 它是圖靈完全的!

那如何證明一個語言是圖靈完全的語言呢?

非常簡單, 如若一門程式設計語言滿足下面三個條件 :

  • 可以進行數值運算和符號運算
  • 擁有判斷結構
  • 擁有迴圈結構

就可以說這門語言是圖靈完全的 (這裡不進行詳細的證明, 如果不同的理解或者異議可以在評論區提出)

首先我們來嘗試數值運算 :

template <typename T, T Value1, T Value2>

struct add {

    constexpr static auto value {Value1 + Value2};      //C++ 11 式

    enum {

        VALUE = value1 + value2     //舊式

    };

};

在沒有特殊的情況下, 今後都會使用 C++ 11 式

然後, 我們來嘗試判斷 :

template <bool Value>

struct if_constexpr {

    //If Value == true, do something

};

template <>

struct if_constexpr<false> {

    //If Value == false, do something

};

當樣板引數為 true, 那麼編碼器將選擇第一個樣板具現化, 否則就會選擇第二個樣板具現化 (這有點類似於 C++ 17 的 if constexpr)

有沒有恍然大悟? 這不就是樣板的特質化嗎~ 沒錯了, 說的那麼高端, 判斷結構, 實際上就是樣板的特質化

最後, 我們來說說迴圈結構, 相對於另外兩個, 這個稍有些複雜 :

template <typename T>

struct remove_extents {

    using result = T;

};

template <typename T>

struct remove_extents<T []> {

    using result = typename remove_extents<T>::result;

};

template <typename T, decltype(sizeof 0) N>

struct remove_extents<T [N]> {

    using result = typename remove_extents<T>::result;

};

我們都知道, 任何的迴圈都會有判斷式, 否則就會進入無止盡的循環無法退出, TMP 的迴圈也不例外. 上述程式碼中, 兩個特質化的類別就是判斷式. remove_extents 是用來將陣列還原為基底型別的一個類別. 例如現在有一個 int [][3][4][5], 若將其作為引數放入 remove_extents 的樣板參數當中, remove_extents<int [][3][4][5]>, 那麼最終的 result 型別就是 int 型別

你可能還沒明白它是如何工作的. 首先, 樣板參數接受了一個引數. 此時, 編碼器就根據具體的型別進行樣板具現化. 當你的型別是一個不帶維度數量的陣列型別, 那麼就會具現化第二個類別樣板; 當你的型別是一個帶維度數量的陣列型別, 那麼就會具現化第三個類別樣板. 而第二個和第三個類別樣板中的 result 又將 T 重新放入 remove_extents 中. 此時, remove_extents 已經完成了移除第一個維度的工作, 就如同你看到的一樣, T 後面的 [] 或者 [N] 被我們捨棄了. 那麼此時, 若將 int [][3][4][5] 放入, result 的型別將是 typename remove_extents<int [3][4][5]>::result. 你會發現, 這其實是一個遞迴的結構, 而不是我們所熟悉的迴圈模式. 是的, 在 TMP 中, 所有的迴圈都將被遞迴所替換. 在完成第一個維度的移除工作之後, 此時又會發現 int [3][4][5] 也是一個帶維度的陣列型別. 那麼又會具現化到第三個類別樣板... 如此不同地重複直到陣列的維度被移除完了, 此時 result 的型別將是 typename remove_extents<int>::result. 對於沒有維度的型別來說, 第一個類別樣板就會被具現化, 這個類別之內不再有迴圈的結構, 而是直接將 int 作為 result 型別, 然後停止迴圈

如果你看懂了上面的舉例, 你會明白, 當放入的型別是一個陣列型別, remove_extents 會不斷地移除第一個維度, 直到型別不再帶有維度. 當型別帶有維度的時候, 就會不斷地具現化第二個或者第三個類別樣板. 當型別不帶維度的時候, 就會具現化第一個樣板, 也就是當型別不帶維度的時候, 停止迴圈

我們在使用 remove_extents 的時候, 就好比這樣 :

int array[1][3][5][6][7][8];

typename remove_extents<decltype(array)>::result integral {0};        //integral 的型別為 int

你可能會發現, 這就像是在呼叫函式一樣. remove_extents 是函式的名稱, <> 中放置函式的引數, 最終回傳了一個結果. 這幾乎和函式的模式是一樣的, 只不過現在, 這個函式是一個類別. 因為它太像函式了, 所以民間給它取了一個名字, 稱它為超函式 (Meta-Function)

在 TMP 中, 你需要理解超函式的概念, 否則的話, 可能會令你寸步難行 : 類別的名稱是超函式的名稱, 放入類別的樣板引數就是超函式的引數, 類別的樣板參數就是超函式的參數. 在上述程式碼中, 我們在類別中宣告了一個別名 result 作為回傳型別, 這也就是超函式的回傳值. 但是在民間, 通常使用 type 作為回傳值, 至少 C++ 標準樣板程式庫中的程式碼就是這樣. 當然, 這個回傳值的名稱你想怎麼取名都可以, 在這裡取名為 result 只是方便大家理解這個 result 就是超函式的最終結果. 對於值來說, value 作為超函式的最終結果, 這應該是沒有任何異議的

那麼, 你又會發現, 樣板超編程既可以操作數值也可以操作型別. 所以在通常的樣板超編程中, 為了統一, 都會把值包裹為型別, 大家通常會建立一個專門用來包裹值的類別 :

template <typename T, T Value>

struct wrapper {

    constexpr static auto value {Value};

};

這個做法是為了讓值轉換為型別, 然後統一對型別進行操作, 以免程式碼重複

當然, 這個系列的文章有時也需要這麼做, 但是我會儘量讓它簡單一些, 以至於大家看起來沒有那麼困難

看到這裡, 有些人難免發問 : TMP 有什麼用呢?

因為 TMP 是編碼器計算的, 所以有時候, 我們可以用它結合編碼器的某些優化來提高程式的性能

例如一個函式是否被 noexcept 標識, 編碼器會根據具體的情況進行優化. 但是可以肯定的是, 被 noexcept 標識的函式在同樣的情況下, 比沒有被 noexcept 表示的函式在性能上要好一些 :

#include <iostream>



using namespace std;



template <typename T>

void func(const T &t) noexcept(is_nothrow_copy_constructible<T>::value) {

    T copy {t};

}

上述程式碼中, 若 T 的型別在複製建構的時候, 是不會擲出例外情況的, 那麼它的性能會比擲出另外情況的版本要稍好一些 (只是一些). is_nothrow_copy_constructible 適用於判別一個型別的複製建構子是否被 noexcept 標識, 我們以後會學習如何編寫它

對於普通的 C++ 程式設計師而言, TMP 的作用非常小的. 因為它通常用於程式庫的設計, 工程計算. 若你是一個追求性能的 C++ 程式設計師, 即使你再普通, TMP 也能幫到你手, 就像上面的示例給出的情況一樣

對於程式庫的作者而言, TMP 幾乎是必不可少的, 因為程式庫需要同時兼顧效率以及相容性; 對於工程計算而言, TMP 的強型別可以讓某些錯誤在編碼器就可以被編碼器檢查出來

因此, TMP 的好處總結起來可以有以下兩點 :

  • 提高程式的性能
  • 將某些運作期的錯誤提前到編碼期

當然了, TMP 帶來了有點也一定有些缺陷 :

  • 程式碼可讀性較差
  • 編碼時間增長
  • 在不同的編碼器情況下, 程式碼移植性不盡相同

到這裡為止, TMP 的介紹已經給大家講解完了, 並且文中的示例也可能讓大家對 TMP 有一些了解了. 作為一個 ending, 這裡將舉例如何在編碼期使用 template 找出一堆數值中的最小值 (基於 C++ 11 的可變參數樣板, C++ 98/03 版本比較複雜, 以後會呈現) :

首先呈現比較簡單的數值版本 :

template <typename T, T ...>

struct find_min;

template <typename T, T Value1, T Value2, T ...Values>

struct find_min<T, Value1, Value2, Values...> {

    constexpr static auto value {find_min<T, find_min<T, Value1, Value2>::value, Values...>::value};

};

template <typename T, T Value1, T Value2>

struct find_min<T, Value1, Value2> {

    constexpr static auto value {Value1 < Value2 ? Value1 : Value2};

};

template <typename T, T Value>

struct find_min<T, Value> {

    constexpr static auto value {Value};

};



#include <iostream>



using namespace std;



int main(int argc, char *argv[]) {

    cout << find_min<int, 1, 2, 3, 4, 5, 0>::value << endl;     //0

    cout << find_min<char, '1', '3', 'd', ' '>::value << endl;      //' '

    //cout << find_min<double, 0.9, 1.2, 0.0>::value << endl;     //編碼錯誤

    wcout << find_min<wchar_t, L'1'>::value << endl;      //'1'

}

接下來是型別運算版本, 這個版本會比數值版本稍為複雜 :

template <typename T, T Value>

struct constant_value {

    constexpr static auto value {Value};

};

template <bool, typename T, typename>

struct if_constexpr {

    using result = T;

};

template <typename T, typename U>

struct if_constexpr<false, T, U> {

    using result = U;

};

template <typename, typename>

struct is_same {

    constexpr static auto value {false};

};

template <typename T>

struct is_same<T, T> {

    constexpr static auto value {true};

};

template <typename T, typename U>

struct operator_less {

    static_assert(is_same<decltype(T::value), decltype(U::value)>::value,

            "Invalid template arguments, the types must be same!");

    using result = typename if_constexpr<(T::value < U::value), T, U>::result;

};

template <typename ...>

struct find_min;

template <typename T, typename U, typename ...Args>

struct find_min<T, U, Args...> {

    using result = typename find_min<typename find_min<T, U>::result, Args...>::result;

};

template <typename T, typename U>

struct find_min<T, U> {

    using result = typename operator_less<T, U>::result;

};

template <typename T>

struct find_min<T> {

    using result = T;

};



#include <iostream>



using std::cout;

using std::endl;

using std::wcout;



int main(int argc, char *argv[]) {

    cout << find_min<constant_value<int, 5>, constant_value<int, 9>, constant_value<int, 99>>::result::value << endl;       //5

    wcout << find_min<constant_value<wchar_t, L'3'>, constant_value<wchar_t, L'l'>>::result::value << endl;     //3

    cout << find_min<constant_value<int, 0>>::result::value << endl;        //0

}

是不是有些嚇到了呢?

其實這只是在數值的外面包裹了一層型別而已, 把這層衣服扒掉, 和數值的版本是一模一樣的. 這兩段程式碼的註解我就不放在上面了, 大家需要自己理解它們

另外, 型別版本的程式碼為了禁止不同的型別相比較, 使用了 C++ 11 的靜態斷言 static_assert. 這是之前在《C++ 學習筆記》中沒有講到的. static_assert 是在編碼期進行檢查的, 第一個引數必須是一個常數表達式, 第二個引數是錯誤提示, 是一個字面值字串. 當第一個常數表達式計算的結果是 false, 那麼此處會產生編碼錯誤. 我們以此來禁止不同型別之間的比較. 如果想要允許不同型別進行比較 (前提是可以比較, 例如 longint 之間是可以比較的, 但是 intint * 之間直接比較是不被允許的), 只需要註解 static_assert 即可

如果實在是無法理解型別版本的程式碼的話, 嘗試一下拆分它們, 把它們拆分為之前所說的運算、判斷式和迴圈再去理解就簡單多了

想要入門 TMP, 上面這些程式碼都是必須看得懂的, 嘗試一下理解它們吧~

 

本文是 TMP 入門的基礎文章, 如果無法理解, 請直接評論, 我會詳細為你講解