在上一篇的教學當中, 我們曾經提到 : 在 TMP 中, 你需要將類別看作是一個函式. 作為一個函式, 除了擁有回傳型別、函式名稱之外, 還需要函式的參數列表

《C++ 學習筆記》中, 我們已經詳細介紹了樣板可以接受普通的樣板引數, 也可以接受非參數型別的樣板引數 :

template <typename T>

struct Foo {};

template <bool Value>

struct Bar {};

這兩種宣告都是合法的, 並且一個樣板參數還可以有一個預設引數

C++ 11 後, C++ 擁有了可變參數樣板 :

template <typename ...Ts>

struct Foo {};

但是在 C++ 引入可變參數樣板之前, 如果我們需要一個參數可變的樣板, 應該如何去實現呢?

首先, 你要清晰地認識到對於一個使用 typename 宣告的樣板參數, 它是可以接受任意完全型別的型別引數的. 為什麼一定要是完全型別呢? 參考以下程式碼 :

template <typename T>

struct Foo {};



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

    Foo<int> f;     //OK

    Foo<Foo> f2;        //編碼錯誤

}

注意到會產生編碼錯誤的那一行程式碼. 這一行程式碼中, 給定 Foo 的樣板引數還是 Foo, 但是不同的是這個 Foo 並沒有給定樣板引數. 這必定會導致編碼錯誤

當然, 值得一提的是, 此處的完全型別和不完全型別和類別的宣告和類別的實作並沒有任何關係. 這裡只是想告訴大家, 在上述情況下, 如果樣板引數還是一個樣板類別, 那麼如果這個樣板類別沒有給定預設的樣板引數, 需要我們給定

回到剛才的討論, 要實現在 C++ 98 / 03 下的可變樣板參數, 通過前面的敘述, 我們知道 : 樣板類別中可以再放入一個樣板類別 (只要給定這個樣板類別的樣本引數即可), 那麼我們自然會想到使用層層疊加的方式 :

template <typename T, typename U>

struct type_list {};

現在, 我想要放入 int, int *, long, double 四個型別到 type_list 的參數中. 顯然地, 我們自然會寫出前面的部分 :

type_list<int,

但是後面如何放入剩下的三個型別呢? 你是否想到巢狀放入呢?

type_list<int, type_list<int *, type_list<long, type_list<double, /* 保留 */>>>>

這就是正確的實作方式. 我們首先看到最外層的 type_list, 它的第一個樣板引數是 int, 第二個樣板引數是 type_list<int *, type_list<long, type_list<double, /* 保留 */>>>; 再看到第二個樣板引數, 它也有自己的樣板引數, 第一個是 int *, 第二個是 type_list<long, type_list<double, /* 保留 */>>; 再看到第二個樣板引數的第二個樣板引數 (你無需會到前面去看, 其實就在分號前面, 我已經複製了一個副本了~), 同樣地, 它的第一個參數是 long, 第二個參數是 type_list<double, /* 保留 */>

看到這裡, 你可能已經明白了, 我們在 C++ 98 / 03 中是通過在樣板引數中巢狀一個樣板引數來實現可變參數樣板的. 再回頭看看 C++ 11 引入的可變參數樣板 :

template <typename ...T>

struct type_container {};

在使用的時候, 直接將四個型別放入即可 :

type_container<int, int *, long, double>

這是多麼方便啊!

當然, 你可能已經注意到了, 最裡面一層的 type_list 少了一個參數. 無需額外的思考, 這必定會造成編碼錯誤. 但是我們確實沒有任何型別需要放入了, 此時應該怎麼辦呢?

通常來說, 我們會宣告一個空的類別 :

struct nil final {};

struct null final {};

struct empty final {};

這個類別可以是任何名稱的, 它裡面沒有包含任何東西, 它只是一個空的類別. 並且, 它不是樣板類別也沒有從其它類別繼承, 甚至其它類別不可從它繼承 (這並不一定)

我們選取 nil 作為我們需要的類別, 這個類別的作用就是填充我們剛剛保留的位置, 也就是參數不夠的地方, 最終有 :

type_list<int, type_list<int *, type_list<long, type_list<double, nil>>>>

現在, 我們只是將 nil 作為一個填充的類別而已. 之後, 我們會知道, nil 除了填充類別以外, 其實還可以作為樣板超編程中遞迴的結束標識, 這裡我們暫時不深入講解

程式設計師都是很懶惰的, 否則這個行業應該沒什麼人吧... 當我們放入 type_list 的型別是大於 1 的奇數或者剛好兩個的時候, 我們可以無需手動使用 nil 填充最後一個空位置 :

type_list<int, <type_list<long, double>>

type_list<double, double>

但是當我們僅使用一個位置或者放入大於 2 的偶數個樣板引數的時候, 我們就需要手動使用 nil 填充了. 我們希望不管什麼時候, 都可以像前面這種情況一樣, 無需填充. 如果你對樣板非常熟悉, 此時你自然想到了樣板可以有預設引數 :

struct nil {};



template <typename T, typename U = nil>

struct type_list {};

此時, 不管放入幾個樣板引數, 我們都無需考慮最後一個空位是否需要我們手動填充的問題

其實現在講解 type_list 有些為時過早, 但是畢竟它並不難, 而且為了平均分擔後面的難度, 現在講也無可厚非. 而且, 在這裡我主要是想要告訴大家 : 樣板引數可以放入另外一個樣板類別甚至是自己, 只要給定了對的型別即可

結束了? 並沒有~ 樣板參數遠沒有你想像的那麼簡單, 否則我也不會獨立拿出一個篇幅來講咯

我們對最開始 Foo 的問題再次深入 :

template <typename T>

struct Foo {};



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

    Foo<Foo> f;

}

通過前面的敘述, 我想你已經瞭解, 這段程式會出現編碼錯誤的情況. 現在, 我想讓這段程式碼通過編碼, 但是我不想給出任何的樣板引數, 應該如何做呢? 這裡就涉及到我們在《C++ 學習筆記》中沒有提到的樣板參數中的樣板參數 :

template <template <typename> typename T>

struct Foo {};

對於樣板參數 T, 我們告訴編碼器, 它一定是一個帶有一個樣板參數的型別 :

template <template <typename> typename T>

struct Foo {};



template <typename>

struct Bar {};



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

    Foo<Bar> f;     //OK

}

此時, 我們放入一個不帶樣板參數的 Bar, 就不會再出現編碼錯誤了 (但是你不可以放入 Foo, 因為這裡的型別不能是自己)

上面的程式碼只是 C++ 17 之後的寫法, 在 C++ 17 之前, 對於樣板參數中的樣板參數, 在宣告的時候只能使用 class 而不能使用 typename :

template <template <typename> class T>

struct Foo {};

我也不知道為什麼有這種設計... 在《Paper Guide》欄目中, 我們也會提及這一新特性

儘管這個系列是以 C++ 11 為基礎, 但是這個地方我們在不必要的情況之下, 都會使用 typename 而並非使用 class

配合 C++ 11 的可變參數樣板, 可以讓樣板中放入任何型別 (在這之前, 你需要完整地給出一個型別) :

template <template <typename ...> typename T>

struct Foo {};



template <typename>

struct Bar {};

template <typename, typename, typename>

struct Bar2 {};

template <typename, typename, typename, typename ...>

struct Bar3 {};



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

    Foo<Bar> f;     //OK

    Foo<Bar2> f2;       //OK

    Foo<Bar3> f3;       //OK

}

 

除此之外, 還有一個內容需要提醒大家 : C++ 的型別非常豐富, 它包含了內建型別 (包括指標、參考、帶有 const 限定的型別、帶有 volatile 限定的型別和函式型別) 和用戶自訂型別. 對於用戶自訂型別來說, 通過上面知識的補充, 已經不再有太大的問題, 而對於內建型別來說, 它比用戶自訂型別更加複雜.

我們已經知道, 指標和參考都算作是一個獨立的型別, 也由此可以推斷帶有 const 限定和帶有 volatile 限定的型別也同樣是一個獨立的型別. 其中, 帶有指標的型別可以是普通的指標, 也可以是函式指標, 更可以是類別成員指標 :

template <typename T>

struct Foo {

    Foo(T) {}

};



int i;

void func() noexcept {}

void func2(int, char) {}


class c {

public:

    int i;

    void func(int, char) volatile noexcept {}

};



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

    Foo<int *> f1(&i);

    Foo<decltype(func) *> f2(func);       //Foo<void (*) noexcept>

    Foo<void (*)(int, char)> f3(func2);

    Foo<int (c::*)> f4(&c::i);

    Foo<void (c::*)(int, char) volatile noexcept> f5(&c::func);

}

從上述程式碼中, 你可以發現, 除了普通的指標, 函式指標和類別物件指標也可以帶有 noexcept 標識; 另外, 類別成員指標還帶有 const 標識和 volatile 標識, 這甚至影響了一個型別的獨立 (帶有 const 標識或者帶有 volatile 標識得函式指標型別與不帶有對應標識的函式指標型別並非同一個型別)

對於函式類別也是相同的. 《C++ 學習筆記》中並沒有提到函式類別也是一種型別, 我們只提到了函式指標型別. 而函式指標型別本質上還是一種指標, 去掉那個指標剩下的才是真正的函式型別 :

我們隨意寫出一個函式 :

constexpr inline void *func(int (*f)(char, long), int i) noexcept {

    if(i == 1) {

        return (void *)f;

    }

    return nullptr;

}

這個函式回傳 void * 型別, 第一個參數接受一個型別為 int (*)(char, long) 的函式指標, 第二個參數接受一個型別為 int 的整型

剛才我們講到, 如果將函式指標型別去掉指標, 就是函式本身的型別. 那麼對於第一個參數來說, 它的型別就是 int (char, long). 正如你所看到的那樣, 函式型別包含了一個回傳型別和參數列表 :

RETURN_TYPE (PARAM_LIST)

其中, PARAM_LIST 代表了函式接受的所有引數對應的型別

既然這樣, 我們自然想到, 樣板可以針對函式型別進行特製化 :

#include <iostream>



using namespace std;

constexpr inline void *func(int (*f)(char, long) noexcept, int i) noexcept {

    if(i == 1) {

        return (void *)f;

    }

    return nullptr;

}

void func2() {

    cout << "hello";

}

template <typename F, typename ...Args>

struct function_caller {

    function_caller(F *f, Args &&...args) {

        f(forward<Args>(args)...);

    }

};

template <>

struct function_caller<void * (int (*)(char, long) noexcept, int)> {

    function_caller() {

        if(not func((int (*)(char, long) noexcept)nullptr, 0)) {

            cout << " world!" << endl;

        }

    }

};

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

    function_caller<decltype(func2)> fc1(func2);

    function_caller<void * (int (*)(char, long) noexcept, int)> fc2;

}

對於第一個物件 fc1, 我們使用了 decltype 推斷 func2 的型別 (之前我們說, 想要使用 decltype 推斷某個函式, 並且獲得這個函式型別對應的函式指標型別必須要在最後加上一個指標, 也就是 decltype(FUNCTION_NAME) *, 我想看到這裡, 閣下也應該明白了其中的道理, 因為 decltype 推斷得出的並不是函式指標型別, 而是一個函式型別), 於是 fc1 的樣板引數就是 void () 型別

上面這個程式碼最終的結果是輸出 : hello world!

 

這篇文章和樣板超編程沒有非常直接的關係, 但是主要是《C++ 學習筆記》中有太多的東西, 我們沒有講到, 而要想學習樣板超編程, 這些知識又是不可或缺的. 文章主要想告訴大家, 樣板參數可以放入給定了樣板引數的樣板類別, 樣板參數的宣告可以是另一個樣板以及函式型別和函式指標型別是相互獨立並且函式型別在 C++ 中是一個合法的型別. 這些知識並不會立即用得上, 但是今後不斷深入你就會發現這些基礎知識的重要性