摘要訊息 : C++ 樣板超編程中超函式的參數列表.

0. 前言

《【C++ Template Meta-Programming】認識樣板超編程 (TMP)》中, 我們曾經提到在 TMP 中, 類別是一個超函式. 那麼既然作為一個函式, 除了擁有回傳型別和函式名稱之外, 還需要函式的參數列表.

更新紀錄 :

  • 2022 年 4 月 17 日進行第一次更新和修正.

1. 一般的型別參數

樣板的參數列表可以放置型別參數, 也可以放置非型別參數. 在 C++ 11 之後, 樣板的參數列表可以是數量可變的 :

template <typename T, typename U>
struct meta_f1;
template <int A, char B = ' '>
struct meta_f2;
template <typename ...Ts>
struct meta_f3;
template <int ...Integers>
struct meta_f4;

除了可變參數樣板之外, 其它不論是型別參數還是非型別參數, 它們都支援有預設引數. 和函式參數類似, 類別樣板的參數列表中的預設引數必須放置在所有參數最後. 類別樣板的參數列表不允許出現中間某個參數有預設引數, 而後面其它的參數沒有預設引數.

2. 類別繼承自類別樣板為自身的類別

C++ 允許一個類別 T 繼承自另外一個類別 U<T> :

template <typename T>
struct A {};
struct B : A<B> {};

在繼承的時候, 實際上類別本身還沒有完成實作, 然而這樣的程式碼是允許的. 即使在類別樣板 A 中有很多用到了樣板參數的地方, 在繼承的時候編碼器並不是先生成 B 然後再去繼承 A<B>, 而是直接把 A 中的可繼承內容全部併入 B 中, 然後一起處理的. 這樣, 所有本來使用樣板參數的地方就全部被 B 所替代, 當然不會產生編碼錯誤.

如果閣下使用過來自標準樣板程式庫標頭檔 <memory>std::enable_shared_from_this, 就會很清楚這樣的用法.

3. C++ 98/03 下的可變樣板參數

真正的可變樣板參數是 C++ 11 引入的, 在 C++ 98/03 中, 為了達到可變樣板參數這樣的目的, 我們通常使用巢狀的 type_list :

template <typename T, typename U>
struct type_list {
    typedef T type;
    typedef U next_type;
};

如果我們想在 type_list 中放入 int, int *, longdouble 四個型別, 那麼我們可以寫成 type_list<int, type_list<int *, type_list<long, double>>>. 但是現在我們如果只放入 int, int *long, 那麼 type_list<int, type_list<int *, type_list<long , ???>>> 其中的 ??? 就沒辦法填寫了. 為了解決這個問題, 我們通常引入一個佔位型別 :

struct nil;

佔位型別的名稱通常是 nil 或者 null, 可以實作也可以不實作 (看具體需求). 這樣, 我們就可以將接收 int, int *longtype_list 寫成 type_list<int, type_list<int *, type<list<long, nil>>>.

由於 C++ 98/03 中類別樣板的樣板參數就支援預設引數, 因此我們通常設定 type_list 的第二個樣板參數為佔位型別 :

struct nil;
template <typename T, typename U = nil>
struct type_list {
    typedef T type;
    typedef U next_type;
};

為了方便程式碼的編寫, 我們通常將接受 int, int *, longdouble 四個型別的 type_list 寫成 type_list<int, type_list<int *, type_list<long, type_list<double, nil>>>>, 而不是像上面一樣直接把 longdouble 放在同一個 type_list 中. 這樣, 在處理迴圈的時候, 我們就以遇到 nil 作為迴圈結束的標誌即可.

在 C++ 11 之後, 我們不用這麼麻煩 :

template <typename ...>
struct type_container;

using char_types = type_container<char, signed char, unsigned char, wchar_t, char16_t, char32_t>;

4. 樣板樣板參數

類別樣板除了可以接受型別引數和非型別引數之外, 還可以接受樣板引數 :

template <template <typename> class T>
struct A {};
template <typename T>
struct B {};
A<B> variable;      // OK

我們注意到 A 的樣板宣告 template <template <typename> class T> 中的 class. 在 C++ 17 之前, 對於樣板樣板參數, 這裡必需使用 class. 但是在 C++ 17 之後, 為了統一起見, 允許這裡使用 typename 替換 class. 在之後的文章當中, 我們統一都使用 typename.

對於可變樣板參數的類別樣板, 樣板樣板參數可以這樣寫 :

template <template <typename ...> typename T>
struct A;
template <typename T, typename U, typename V>
struct B1;
template <typename T, typename U, typename ...Ts>
struct B2;
template <typename ...Ts>
struct B3;

using A_B1 = A<B1>;     // OK
using A_B2 = A<B2>;     // OK
using A_B3 = A<B3>;     // OK

另外, 樣板參數中樣板, 例如 Code 7 中的 T, 它的樣板參數名稱我們已經省略掉了, 因為完全沒有用處. T 的樣板參數只有在其具現化為真正的型別的時候才有用, 而在 A 中並沒有.

5. 複雜的型別系統

C++ 的型別系統非常複雜, 就算是不算上用戶自訂型別, 內建型別也非常複雜. 首先, C++ 的型別分為幾個大類 : void 型別, 整型型別, 浮點數型別, 左值參考, 右值參考, 指標型別 (包含函式指標型別), 指向類別成員的指標型別, 陣列型別和函式型別. 這些型別還可以被 const, volatile 標識 (包括 void 型別).

另外, 函式型別和函式指標型別是不同的. 放入 std::function 中的那個是函式型別, 例如 void (int). 函式型別只有一個回傳值和參數列表, 而函式指標型別在回傳型別和參數列表之間還有一個指標符號, 例如 void (*)(int). 在 C++ 11 中, 函式型別不可以被 noexcept 標識, 但是函式指標型別可以被 noexcept 標識, 但是 noexcept 並會對型別產生影響. 因此在 C++ 11 中, void (*)(int)void (*)(int) noexcept 屬於同樣的型別. 但是在 C++ 17 中, noexcept 開始影響函式型別. 於是在 C++ 17 之後, void (int) noexcept 就是一個合法的函式型別 (它在 C++ 11 中會發生編碼錯誤), 而 void (int) noexceptvoid (int) 就不再屬於同一個型別. 類似地, void (*)(int)void (*)(int) noexcept 在 C++ 17 中就不再屬於同樣的型別.

我們知道, 函式指標型別除了可以被 noexcept 標識之外, 指向類別成員的函式指標還可以被 const, volatile 和參考符號標識. 在 C++ 11 中, 被 const, volatile 或者被參考符號標識的函式指標和沒有標識的函式指標不屬於同一個型別. 例如 void (Class::*)(int) constvoid (Class::*)(int) 不屬於同一個型別. 其中, Class 是類別型別. 這些成員函式指標對應的函式型別為 void (int) constvoid (int), 它們同樣不屬於同一個型別.

這樣, 我們就解釋了為什麼要使用 decltype 宣告一個函式指標必須在後面加一個指標符號 *. 因為 decltype 推導而來的是函式型別, 而不是函式指標型別.