我們已經講述了 C++ 樣板超編程的基礎知識, 我認為自己實作 C++ 標準樣板程式庫中的標頭檔 <type_traits> 非常重要, 這能夠帶你更加深入地理解 C++ 樣板超編程

帶有 std:: 名稱空間的是來自 C++ 標準樣板程式庫中的樣板類別, 不帶有名稱空間的是我自己認為需要加入到這篇文章中的

1. std::integral_constant

在 C++ 20 之後, C++ 引入了 非型別樣板參數 (P0732R2). 我們之後會在《Paper Guid》欄目中進行敘述, 此處我們暫時不展開, 但是我可以用一段程式碼進行演示 :

struct A {};
template <A a>      //C++ 20
struct Foo {};

上述程式碼在 C++ 17 之前會導致編碼錯誤, 但是在 C++ 20 中是可以通過編碼的. 而 std::integral_constant 相對於這個特性稍微有些過時, 因為原來樣板中的參數只能是型別或者整型引數, 但是現在這個限制被放開, 我們可以嘗試實作一個樣板類別 constant. 它可以持有一個編碼期已知的常數表達式, 並且可以向其發生隱含型別轉化, 最終結果就是那個場數表達式最終的值. 除此之外, 我們還希望為其多載函式呼叫運算子, 提供和隱含型別轉化一樣的功能 :

template <typename T, T Value>
struct constant {
    using value_type = T;
    using type = constant;
    constexpr static auto value {Value};
    constexpr value_type operator()() const noexcept {
        return this->value;
    }
    constexpr operator value_type() const noexcept {
        return this->value;
    }
};

2. std::bool_constant, std::true_typestd::false_type

std::bool_constantstd::integral_constant 針對 bool 型別的偏特製化版本 :

template <bool Value>
using bool_constant = constant<bool, Value>;

布林型別有且唯有兩個值, 因此我們可以通過列舉來實作出持有布林值的所有可能的類別. 而 std::true_typestd::false_type 在之後的實作中扮演者重要的角色, 它用於標記某個型別是否存在某個特性

using true_type = bool_constant<true>;
using false_type = bool_constant<false>;

3. std::conditional

我們其實在文章《【C++ Template Meta-Programming】認識樣板超編程 (TMP)》中已經實作過它了, 但是當時我們取的名字為 if_constexpr :

template <bool, typename If, typename>
struct conditional {
    using type = If;
};
template <typename If, typename Then>
struct conditional<false, If, Then> {
    using type = Then;
};

4. std::enable_if

我們在文章《【C++ Template Meta-Programming】到處 SFINAE》中已經實作過它, 它通常被用於 SFINAE :

template <bool, typename = void>
struct enable_if;
template <typename T>
struct enable_if<true, T> {
    using type = T;
};

5. make_voidstd::void_t

我們在文章《【C++ Template Meta-Programming】到處 SFINAE》中已經實作過 make_void, 它也通常被用於 SFINAE. 對於 make_void, 其功能和 std::void_t 一樣, 只不過它是一個類別樣板, 而 std::void_t 是一個型別別名 :

template <typename ...>
struct make_void {
    using type = void;
};
template <typename ...>
using void_t = void;

6. std::type_identitytype_holder

這兩個類別樣板的作用相同, 都是用於持有某一個型別 :

template <typename T>
struct type_identity {
    using type = T;
};
template <typename T>
using type_holder = type_identity<T>;

7. make_true_type, make_false_type, make_truemake_false

這幾個 make 系列的類別樣板和 make_void 相同, 只不過 make_true_typemake_false_type 是超函式最終回傳結果為 std::true_type 或者為 std::false_type. 而 make_truemake_false 使用 C++ 14 引入的變數樣板, 最終的結果是 bool 型別的常數表達式 :

template <typename ...>
struct make_true_type {
    using type = true_type;
};
template <typename ...>
struct make_false_type {
    using type = false_type;
};
template <typename ...>
constexpr inline auto make_true {true};
template <typename ...>
constexpr inline auto make_false {false};

8. type_container

這是一個容器, 用於持有一系列的型別, 就像 std::vector 持有若干個元素一樣. 我們一般使用 C++ 11 引入的可變樣板參數來實作. 但是我們要考慮的是

  • 如何獲取當前的型別 T
  • 如何獲取除了 T 之外, 位於型別容器中的剩餘型別
  • 如若型別容器僅持有一個型別, 那麼剩餘型別應該如何表示
  • 如果沒有給定樣板引數, 應該如何處理

獲取當前型別, 這實際上就是超函式的回傳值, 實作和上面幾個類別一樣. 對於剩餘的型別, 我們可以把其重新放進 type_container 中, 然後用一個型別別名代表它. 實際上, 這就是超函式有了兩個回傳值

對於沒有給定樣板引數的 type_container, 它一般用於結束標誌, 我們要求結束標誌必須是獨一無二. 我們可以宣告一個新的類別, 例如 struct unique;, 然後把它用於結束標誌. 但是可變參數樣板允許樣板引數為空, 而且當樣板引數為空的時候, 它和任意其它型別都不同. 因此我們可以直接使用 type_container<> 作為結束標誌

有了上述討論, 我們就可以實作處 type_container :

template <typename ...Args>
struct type_container;
template <>
struct type_container<> {
    using type = type_container<>;
    using remaining = type_container<>;
};
template <typename T>
struct type_container<T> {
    using type = T;
    using remaining = type_container<>;
};
template <typename T, typename ...Args>
struct type_container<T, Args...> {
    using type = T;
    using remaining = type_container<Args...>;
};

9. unique_type

編碼器會為每一個 lambda 表達式生成一個獨一無二的類別. 也就是說, 我們雖然寫了一個 lambda 表達式, 但是編碼器必須進行轉換才行, 編碼器所做的就是為這個 lambda 表達式撰寫一個多載了函式呼叫運算子的類別. 因此, 我們可以將其用於樣板參數來生成獨一無二的類別 :

template <void () = [] {}>
struct unique_type {};

我們使用 std::is_same 來測試每一次使用的 unique_type 是否都是不同的型別 :

int main(int argc, char *argv[]) {
    cout << std::is_same<unique_type<>, unique_type<>>::value << endl;      //輸出 : 0
}

10. std::add_const, std::add_volatilestd::add_cv

為某個型別添加 const 或者 volatile 限定比較簡單, 因為對於任何型別都可以添加 constvolatile 限定, 而且當在某個出現多個 const 或者 volatile 時, 編碼器會幫助我們去處多餘的 constvolatile, 而不會產生編碼錯誤

std::add_cv 是為某個型別同時添加 constvolatile 限定符

template <typename T>
struct add_const {
    using type = const T;
};
template <typename T>
struct add_volatile {
    using type = volatile T;
};
template <typename T>
struct add_cv {
    using type = typename add_const<typename add_volatile<T>::type>::type;
};

11. std::add_lvalue_reference, std::add_rvalue_reference, std::add_pointer, add_const_referenceadd_const_pointer

為什麼上面幾個 add 系列的超函式我要和第 10 中的 add 系列超函式區別開來呢? 因為我們已經不能像第 10 中的實作方法一樣, 簡單得為某個型別添加參考或者指標. 在絕大多數情況下, 直接添加參考或者指標是可行的, 但是我們必須考慮那些少數情況

對於參考來說, 容易地, 我們想到 void 型別不存在參考型別. 另外, 針對函式型別來說, 如果其帶有 constvolatile 或者參考限定, 那麼這個函式型別也不能再為其添加參考限定, 否則會導致編碼錯誤

對於指標來說, 容易地, 我們想到不存在參考的指標. 另外, 針對函式型別來說, 它和 add_reference 系列的超函式有著同樣的情況. 如果其帶有 constvolatile 或者參考限定, 不存在這些型別的指標

對於部分型別, 當為其添加指標或者參考失敗的時候, 我們不希望產生編碼錯誤, 而是希望最終回傳結果是原來的型別. 那麼我們想到使用 SFINAE, 同時借助 decltype 推導函式的回傳型別. 這裡, 我僅寫出 std::add_lvalue_reference 的實作 :

template <typename T>
typename type_identity<T &>::type test_referencable(int) noexcept;
template <typename T>
T test_referencable(...) noexcept;
template <typename T>
struct add_lvalue_reference {
    using type = decltype(test_referencable<T>(0));
};

雖然我們使用了 SFINAE, 但是函式 test_referencable 的參數列表並不能一樣, 因為當 T & 有效的時候, 編碼期無法判斷選擇哪一個函式, 最終會導致編碼錯誤. 因此, 這裡我們還需要進行多載

add_const_referenceadd_const_pointer 是組合多個 add 系列的超函式. 這裡要特別拿出來講是因為這裡要注意添加的順序, 順序不正確會導致結果不正確. 針對 T * 來說, 它是指標型別, 為其添加指標, 結果是 T *const. 而我們希望得到的結果是 const T *. 因為應該這樣去實作 :

template <typename T>
struct add_const_pointer {
    using type = typename add_pointer<typename add_const<T>::type>::type;
};

先添加參考, 然後才添加指標

add_const_reference 可能會導致歧異, 因為參考分為左值參考和右值參考. 而一般來說, 為右值參考添加 const 限定沒有任何意義. 因此, 一般來說我們所說的 const_reference 是指一個添加了 const 限定的左值參考. 除此之外, 添加參考時, 也需要注意順序問題. 具體的實作和上面一樣, 這裡我不再累贅

12. std::remove_lvalue_reference, std::remove_rvalue_reference, std::remove_reference, std::remove_pointer, std::remove_const, std::remove_volatile, std::remove_cv, remove_const_referencestd::remove_cvref

remove 系列的函式還是非常好實作的, 我們知道樣板的偏特製化的時候, 會針對給定的型別匹配到對應的偏特製化後的類別中. 利用這個特性, 我們可以實作 std::remove_lvalue_reference :

template <typename T>
struct remove_lvalue_reference {
    using type = T;
};
template <typename T>
struct remove_lvalue_reference<T &> {
    using type = T;
};

剩下別的我就不實作了, 因為幾乎和上面的實作方式一樣. 這裡要特別指出, cv 是指 const 以及 voaltile 限定, 也就是 std::remove_cv 要同時移除 const 限定和 volatile 限定

最後, remove_const_referencestd::remove_cvref 也有順序問題. 對於型別 const T &, 它是一個參考型別, 而不是一個帶有 const 限定的型別. 也就是說, std::is_reference<const T &>::value 的結果為 true, 而 std::is_const<const T &>::value 的結果為 false. 因此, 根據這兩個結果, 我們首先要移除參考, 然後去移除 const 限定或者 volatile 限定 :

template <typename T>
struct remove_const_reference : remove_const<typename remove_reference<T>::type> {};

這裡, 我們使用了繼承, 這是為了寫少了一些程式碼, 最終的效果是一樣的

12. std::remove_extentremove_extents

兩個名稱就是結尾有沒有 s 的區別, 因此一個是移除一個陣列維度, 另外一個是移除全部陣列維度. remove_extents 在 C++ 標準樣板程式庫中對應 std::remove_all_extents. 對於 std::remove_extent 來說, 我們需要考慮帶有大小的維度和不帶有大小的維度, 因為這兩個雖然在函式中傳遞的時候是一樣的, 但是型別是不一樣的. 因此, 針對 std::remove_extent, 我們需要兩個偏特製化版本 :

template <typename T>
struct remove_extent {
    using type = T;
};
template <typename T>
struct remove_extent<T []> {
    using type = T;
};
template <typename T, size_t N>
struct remove_extent<T [N]> {
    using type = T;
};

對於 remove_extents 來說, 它需要不斷地移除一個陣列維度, 直到型別不是一個陣列為止. 而類別在偏特製化, 如果遇到非陣列型別, 會自動匹配到未特製化的版本 :

template <typename T>
struct remove_extents {     //如果 T 不是陣列型別, 那麼會自動匹配到這個未被特製化的版本
    using type = T;
};

如果給定的型別是特製化版本, 只要我們一直讓它匹配到偏特製化的版本上就可以了 :

template <typename T>
struct remove_extents {
    using type = T;
};
template <typename T>
using remove_extents_t = typename remove_extents<T>::type;
template <typename T>
struct remove_extents<T []> {
    using type = remove_extents_t<T>;
};
template <typename T, size_t N>
struct remove_extents<T [N]> {
    using type = remove_extents_t<T>;
};

上面我們使用了型別別名也是可以寫少幾個 typename::type

13. std::is_same

is 系列的函式, 其內部都帶有一個 bool 型別的靜態成員變數, 表明結果是 true 還是 false. 因此, 我們直接讓這些類別繼承 std::true_type 或者 std::false_type 即可. 不需要重新去寫一個類似於 constant 的類別

std::is_same 用於判定兩個給定的型別是否為同一個型別, 利用偏特製化, 我們容易寫出 :

template <typename, typename>
struct is_same : false_type {};
template <typename T>
struct is_same<T, T> : true_type {};

14. std::is_const, std::is_volatile, is_cv, std::is_lvalue_reference, std::is_rvalue_reference, std::is_reference 和 std::is_pointer

這幾個超函式用於判定一個型別是否帶有 const 或者 volatile 限定符. 這裡要特別注意, is_cv 對於同時帶有 constvolatile 限定符才回傳 true, 否則回傳 false. 而 std::is_reference 是當型別是左值參考或者右值參考時, 都回傳 true. 這裡僅僅實作 std::is_const :

template <typename T>
struct is_const : false_type {};
template <typename T>
struct is_const<const T> : true_type {};

15. std::is_voidis_null_pointer

is_null_pointer 在 C++ 標準樣板程式庫中叫 std::is_nullptr. 這兩個超函式用於判定給定的型別是不是 void 型別或者 nullptr_t 型別. 實際上非常簡單, 但是有一個需要注意的地方是帶有 const 或者 volatile 限定符的 void 型別或者 nullptr_t 型別, 也能夠使得 std::is_void 或者 is_null_pointer 回傳 true. 因此, 我們借用 std::is_same :

template <typename T>
struct is_void : is_same<void, remove_cv_t<T>> {};
template <typename T>
constexpr inline auto is_void_v {is_void<T>::value};

template <typename T>
struct is_null_pointer : is_same<decltype(nullptr), remove_cv_t<T>> {};
template <typename T>
constexpr inline auto is_null_pointer_v {is_null_pointer<T>::value};

16. std::is_integralstd::is_unsignedstd::is_signedis_characterstd::is_floating_point

這幾個超函式都有比較多的型別需要特製化. 和 std::is_voidis_null_pointer 相同, 這幾個函式同樣需要移除 constvolatile 限定. C++ 中屬於整型的型別有 : boolcharsigned charunsigned charwchar_tchar16_tchar32_tchar8_t (C++ 20)、shortunsigned shortintunsigned intlongunsigned longlong longunsigned long long. 某些編碼器可能會內建一個 128 位元的整型型別 : __int128_t__uint128_t. 這裡要特別注意, signed charchar 是屬於不同的型別, unsigned charchar 也是屬於不同的型別. C++ 中屬於浮點數型別的有 : floatdoublelong double

這裡, 我選擇 std::is_floating_point 作為示例進行實作 :

template <typename T>
struct is_floating_point_impl : false_type {};
template <>
struct is_floating_point_impl<float> : true_type {};
template <>
struct is_floating_point_impl<double> : true_type {};
template <>
struct is_floating_point_impl<long double> : true_type {};
template <typename T>
struct is_floating_point : is_floating_point_impl<typename remove_cv<T>::type> {};

17. std::is_array, std::is_bounded_arraystd::is_unbounded_array

bounded 和 unbounded 的區別就是陣列型別中的某一維度是否帶有大小. 而陣列型別不需要考慮其是否帶有 constvolatile 或者參考限定, 因此實作比較簡單 :

template <typename>
struct is_array : false_type {};
template <typename T>
struct is_array<T []> : true_type {};
template <typename T, size_t N>
struct is_array<T [N]> : true_type {};

18. std::is_enum, std::is_union, std::is_class, std::is_final, std::is_empty, std::is_standard_layout, std::is_abstract, std::is_polymorphic, std::is_aggregate, std::is_literal_type, std::is_base_of, std::is_trivial, std::has_virtual_destructor, std::is_pod, std::is_trivially_copyable, std::is_trivially_constructible, std::is_trivially_assignablestd::is_trivially_destructible

有些類別從名字可能無法猜到其具體的用途, 這裡我解釋一下

  • std::is_empty : 如果某個類別中沒有任何東西, 那麼它就是空的, std::is_empty 會回傳 true
  • std::is_standard_layout : C++ 中例如虛擬函式等一些機制可能會導致類別的配置和 C 語言不相容, 這個超函式用於判別某個類別是否具有和 C 語言相容的標準配置
  • std::is_abstract : 如果某個類別是抽象基礎類別, 則 std::is_abstract 會回傳 true
  • std::is_polymorphic : 如果某個類別是多型的 (一般帶有虛擬函式或者虛擬基礎類別), 那麼 std::is_polymorphic 會回傳 true
  • std::is_aggregate : 如果某個型別滿足下列要求 (C++ 20 標準, 這種類別稱為聚合類別), 那麼 std::is_aggregate 會回傳 true :
    • 陣列型別
    • 對於類別來說, 至少滿足 :
      • 不存在 privateprotected 的非靜態成員
      • 不存在用戶自訂或者繼承的建構子
      • 不存在虛擬基礎類別以及私用繼承的基礎類別
      • 不存在虛擬成員函式
  • std::is_literal_type : 如果某個型別滿足下列要求 (C++ 20 標準), 那麼 std::is_literal_type 會回傳 true :
    • void 型別
    • 參考型別
    • 非參考的陣列型別
    • 算數型別
    • 列舉型別
    • 指標型別
    • nullptr_t 型別
    • 對於類別來說, 至少滿足一項 :
      • 聚合類別
      • 除複製建構子和移動建構子之外, 至少存在一個被 constexpr 標識的建構子
      • lambda 表達式對應的閉包型別
      • 對於等位 union, 至少存在一個非靜態且未被 volatile 標識的字面值成員變數
      • 對於非等位的類別, 不能存在被 volatile 標識的字面值成員變數
      • 帶有 constexpr 標識的解構子
  • std::is_trivial : 如果某個型別滿足下列要求, 那麼 std::is_trivial 將會回傳 true :
    • 沒有虛擬基礎類別
    • 沒有虛擬成員函式
    • 基礎類別和所有成員變數的建構子、多載的指派運算子和解構子都必須是 trivial 的 (trivial 就是無意義的, 你可以不去自訂. 我們一般不給超函式定義建構子, 因此我們可以說超函式的建構子是 trivial 的)
  • std::is_pod : 如果型別滿足下列要求 (POD 型別), 那麼 std::is_pos 會回傳 true :
    • 算數型別 (包括整型型別和浮點數型別)
    • 指標型別
    • 列舉型別
    • nullptr_t 型別
    • 對於類別來說, 至少滿足 :
      • 是一個 trivial 的型別
      • 是一個標準配置 (standard layout) 的型別
      • 所有非靜態成員變數都是 POD 型別
    • 陣列型別

看似這些超函式都是毫不相干, 並且實作起來非常困難的. 但是實際上, 絕大部分超函式我們是無法進行實作. 只有編碼器才知道我們給定的型別到底是否滿足這些要求. 因此, 編碼器貼心地為我們提供了一些看似是關鍵字的編碼器魔法 :

template <typename T>
struct is_enum : bool_constant<__is_enum(T)> {};

我們只需要在名稱前面加兩個下劃線, 然後和 sizeof 差不多的形式去使用它們, 參數列表是一些型別, 就可以獲得一個 bool 型別的結果. 由於有了這個編碼器魔法之後, 實作變得特別簡單, 因此剩餘的超函式實作我不再累贅

針對有些我們可以自行完成的超函式 : std::is_convertiblestd::is_constructiblestd::is_assignablestd::is_nothrow_constructiblestd::is_nothrow_assignable, 編碼器也為我們提供了上面的魔法. 不過既然我們可以自行完成, 我也會在下面進行講解. 需要注意的是, 對於 std::is_convertible, 編碼器 Clang++ 提供的魔法版本的名稱為 __is_convertible_to

另外, 由於這些魔法是編碼器自訂的, 屬於實作定義行為, 所以不同編碼器提供的魔法名稱可能稍有不同

19. is_type

對於任意型別, is_type 都回傳 true. 它一般用於樣板中的延時計算, 這涉及到樣板的二階段名稱查找, 此處暫時不具體展開 :

template <typename T>
struct is_type : true_type {};

20. is_complete

這個超函式用於判定某個型別是否已經完成, 特別是針對類別. 某些類別可能在當前需要判定它是否已經被實作, 因此可以借助這個超函式進行判定. 判定的方式是借助 sizeof, 對於未實作的型別, 對其運用 sizeof 運算子會導致編碼錯誤. 結合 decltype 和 SFINAE, 我們可以寫出 :

template <typename, typename T>
struct select_second_type {
    using type = T;
};
template <typename T>
typename select_second_type<decltype(sizeof(T)), true_type>::type test_complete(int) noexcept;
template <typename T>
false_type test_complete(...) noexcept;
template <typename T>
struct is_complete : decltype(test_complete<T>(0)) {};

這裡我們用到了一個 select_second_type 的輔助超函式. 因為我們最終需要的結果型別是 true_type 或者 false_type, 而不是 sizeof(T) 的回傳值或者回傳值的型別. 因此, 我們把 decltype(sizeof(T)) 作為 select_second_type 作為第一個樣板引數. 如果 T 是已經實作的型別, 那麼 select_second_type 可以被正常推導; 當其無法進行推導, 即遇到了 sizeof(T) 會產生編碼錯誤的時候, 編碼器會直接忽略這個函式, 從而匹配到回傳型別為 false_typetest_complete 函式版本

21. std::is_function

我們一般講到函式型別, 一般都會聯想到函式指標. 但是當我們將函式指標放入 std::remove_pointer 中之後, 得到的是一個完完全全的函式型別. 大家可能對函式型別非常陌生, 因為幾乎用不到函式型別. 在 C++ 中, 函式型別和陣列型別一樣, 具有衰退的特性, 可以隱含地轉變為函式指標型別 :

void f(void func()) {}
void f(void (*func)()) {}

這兩行程式碼同時出現在同一可視範圍內時, 會產生編碼錯誤. 因為 void () 會褪化為 void (*)(), 這和 int [] 的陣列在函式參數列表中褪化為 int * 一樣

然而函式的型別非常複雜. 要判定一個型別是否為函式型別, 首先要知道它的參數列表. 在樣板超編程中, 我們甚至沒有辦法知道給定的函式型別的參數列表中有多少參數, 參數分別是什麼型別. 因此, 我們需要借助 C++ 11 引入的可變參數樣板 :

template <typename>
struct is_function : false_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...)> : true_type {};

由於 C++ 與 C 相容, 所以 C++ 必須支援 C-Style 的可變參數列表, 即函式參數最後是以省略號結尾的. 上面的可變參數樣板只能對存在於參數列表中, 省略號之前的參數型別進行推導, 無法針對省略號中的參數型別進行推導. 但是, 我們其實可以直接將省略號用於函式型別中 :

template <typename R, typename ...Args>
struct is_function<R (Args..., ...)> : true_type {};

除了上面這個寫法之外, 還有一種寫法 :

template <typename R, typename ...Args>
struct is_function<R (Args......)> : true_type {};

Args...... 會展開成為 Args..., .... 不過在有些編碼器下, 這種寫法會有編碼警告被擲出

另外, noexcept 會影響函式的型別, 因此還需要針對帶有 noexcept 的函式型別進行偏特製化 :

template <typename R, typename ...Args>
struct is_function<R (Args...) noexcept> : true_type {};

對於類別的成員函式, 其還可以帶有 constvolatile 和參考限定, 因此我們還需要對這些進行偏特製化. 這裡僅給出一種實例 :

template <typename R, typename ...Args>
struct is_function<R (Args..., ...) const volatile && noexcept> : true_type {};

對於函式型別 :

  • 是否具有 C-Style 的可變參數列表
  • 是否被 noexcept 標識
  • 是否帶有 const 限定符
  • 是否帶有 volatile 限定符
  • 是否帶有參考限定符
  • 以及上面幾種的不同組合

我們要將上面提到的所有可能的型別都列舉一邊, std::is_function 才算完成 :

template <typename>
struct is_function : false_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...)> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...)> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) const> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) volatile> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) const volatile> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) const> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) volatile> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) const volatile> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) &> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) const &> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) volatile &> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) const volatile &> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) &> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) const &> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) volatile &> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) const volatile &> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) &&> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) const &&> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) volatile &&> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) const volatile &&> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) &&> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) const &&> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) volatile &&> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) const volatile &&> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) const noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) volatile noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) const volatile noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) const noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) volatile noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) const volatile noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) & noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) const & noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) volatile & noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) const volatile & noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) & noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) const & noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) volatile & noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) const volatile & noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) && noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) const && noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) volatile && noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args...) const volatile && noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) && noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) const && noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) volatile && noexcept> : true_type {};
template <typename R, typename ...Args>
struct is_function<R (Args..., ...) const volatile && noexcept> : true_type {};

22. std::is_member_pointer, std::is_member_function_pointerstd::is_member_object_pointer

對於指向成員的指標, 它們雖然都是指標型別, 但是它有著特殊的寫法 : T Class::*. 因此, 我們可以對 std::is_member_pointer 進行實作 :

template <typename T>
struct is_member_pointer : false_type {};
template <typename T, typename Class>
struct is_member_pointer<T Class::*> : true_type {};

判定一個型別是否為成員函式指標, 其基本形式是 F Class::*, 其中我們期望 F 是函式型別. 因此, 我們可以借助 std::is_function 來判定 :

template <typename>
struct is_member_function_pointer_auxiliary : false_type {};
template <typename F, typename Class>
struct is_member_function_pointer_auxiliary<F Class::*> : is_function<F> {};

對於指向成員物件的指標型別來說, 首先它必須是指標, 其次只要它不是指向成員函式的, 那麼就必定是指向成員物件的 :

template <typename T>
struct is_member_object_pointer : bool_constant<is_pointer<T>::value and not is_member_function_pointer<T>::value> {};

23. std::is_function_pointer

對於一個型別來說, 要想 std::is_function_pointer 的回傳值為 true, 那麼首先它必須是一個指標型別. 其次, 我們再考慮, 一個 void * 指標和一個 nullptr_t * 指標同樣可以容納指向函式的指標, 因此它們應該也能算進來. 最後, 如果型別的指標被移除之後, 它必須是一個函式型別. 根據這個思路, 我們可以這樣進行實作 :

template <typename T>
struct is_function_pointer : bool_constant<is_pointer<T>::value and (is_function<typename remove_pointer<T>::type>::value or is_void<typename remove_pointer<T>::type>::value or is_null_pointer<typename remove_pointer<T>::type>::value)> {};

24. std::is_constructible, std::is_default_constructible, std::is_copy_constructible, std::is_move_constructible, std::is_assignable, std::is_copy_assignable, std::is_move_assignable, std::is_destructible, std::is_swappable_withstd::is_swappable

這些超函式的實作實際上我們上面已經講述過了, 需要利用 decltype 和 SFINAE. 但是我們需要產生一個型別的右值才能實作這些超函式, 因此我們還需要實作 std::declval. std::declval 是一個函式宣告, 一般來說它不會被定義, 而且是不允許被呼叫的, 就像我們上面寫的 test_referencable 一樣. 對於 test_referencable, 我們需要使用 decltype 推導其回傳型別, 也就是說, test_referencable 在語義上其實得到了一個回傳值, 它才能被推導. 同理, 如果我們要獲得這個回傳值, 就可以把它用於任何需要引數的地方. 假設現在有一個函式 :

char &func(int, int);

我們希望可以得到其回傳型別, 因此一般我們會這樣去獲得 : decltype(func(1, 1)). 但是現在我們是知道了這個函式接受兩個型別為 int 的引數, 在樣板超編程中我們通常是不知道函式的參數列表對應的型別的, 因此, 我們需要使用 std::declval 產生參數列表對應每個型別的右值 :

template <typename ...Args>
UNKNOWN func(Args ...);
template <typename ...Args>
using result_type = decltype(func(std::declval<Args>()...));

現在, 你可能可以理解為什麼我剛才說的需要產生一個型別的右值才能實作這些超函式. 以 std::is_constructible 舉例, 建構子不一定只有一個, 因此對於任意類別 C, 其建構子的參數列表我們是不知道的, 要測試這個建構子是否存在, 我們需要借助 SFINAE :

template <typename, typename T>
struct select_second_type {
    using type = T;
};
template <typename T, typename ...Args>
typename select_second_type<decltype(T(std::declval<Args>()...)), true_type>::type test_constructible(int) noexcept;
template <typename ...>
false_type test_constructible(...) noexcept;

T(...) 是一個類別的建構子呼叫方式, 最終會得到這個型別對應的右值物件, 省略號中要放入的就是對應建構子的引數. 通過上述方式, 我們可以測試某一個建構子的呼叫是否是可以成功的, 如果成功, 其回傳型別為 std::true_type; 否則, 為 std::false_type

於是, std::is_constructible 可以實作為

template <typename T, typename ...Args>
struct is_constructible : decltype(test_constructible<T, Args...>(0)) {};

對於 std::is_default_constructible 只要令參數列表為空即可 :

template <typename T>
struct is_default_constructible : is_constructible<T> {};

對於 std::is_copy_constructible, 只要令參數列表為帶有 const 限定的類別參考型別即可 :

template <typename T>
struct is_copy_constructible : is_constructible<T, typename add_const_reference<T>::type> {};

對於 std::is_assignable, 我們可以修改 test_constructible, 把 select_second_type 中的第一個引數修改一下即可 :

template <typename, typename T>
struct select_second_type {
    using type = T;
};
template <typename T, typename U>
typename select_second_type<decltype(std::declval<T>() = std::declval<U>()), true_type>::type test_assignable(int) noexcept;
template <typename, typename>
false_type test_assignable(...) noexcept;

我們發現, 我們已經有能力判定任何函式的呼叫了. 比如我要檢測是否存在一個名為 func 的成員函式, 我們就寫為 :

template <typename T, typename ...Args>
typename select_second_type<decltype(std::declval<T>().func(std::declval<Args>()...)), true_type>::type test_func(int) noexcept;
template <typename ...>
false_type test_func(...) noexcept;

只需要修改 select_second_type 中第一個引數就可以了. 依樣畫葫蘆, 對於 std::is_destructiblestd::is_convertible, 我們可以這樣去實作輔助函式 :

template <typename, typename T>
struct select_second_type {
    using type = T;
};
template <typename T>
typename select_second_type<decltype(std::declval<T &>().~T()), true_type>::type test_destructible(int) noexcept;
template <typename>
false_type test_destructible(...) noexcept;
template <typename From, typename To>
typename select_second_type<decltype((To)std::declval<From>()), true_type>::type test_convertible(int) noexcept;
template <typename, typename>
false_type test_convertible(...) noexcept;

對於 std::is_destructible, 還需要一些判斷. 對於函式型別、void 型別以及存在未知維度的陣列型別, 它們永遠無法被解構, 因此 std::is_destructible 針對它們的回傳值應該為 false. 對於參考型別, 儘管其看起來不能被解構, 但是它低層實際上是指標, 所以它應該總是可以被解構. 對於其它型別, 移除所有陣列維度之後, 再去檢測其是否存在解構子 :

template <typename T>
struct is_destructible : conditional_t<
    not is_function_v<T> and not is_unbounded_array_v<T> and not is_same_v<void, T>,
    conditional_t<
        is_reference_v<T>,
        true_type,
        conditional_t<
            is_complete_v<T>,
            decltype(__dsa::test_destructible<remove_extents_t<T>>(0)),
            false_type
        >
    >,
    false_type
> {};

除此之外, 我們甚至可以寫一個 is_list_constructible, is_static_castable, is_dynamic_castable, is_const_castable, is_reinterpret_castableis_c_style_castable

現在的問題就在於如何實作 std::declval, 其實非常簡單. 我們實際上要產生某個型別的右值, 因此對於可以為其添加右值參考的型別, 就為其添加右值參考, 然後作為 std::declval 的回傳型別; 否則, 就把型別本身作為 std::declval 的回傳型別 :

template <typename T>
T &&declval_auxiliary(int) noexcept;
template <typename T>
T declval_auxiliary(...) noexcept;
template <typename T>
decltype(__dsa::declval_auxiliary<T>(0)) declval() noexcept;

那麼為什麼不是直接回傳 T 而是要回傳 T 的右值參考呢? 考慮下面這個實例 :

template <typename T>
T declprval() noexcept;
class A;
void f(A &&);
decltype(f(declprval<A>())) *p;     //Compiler Error : calling 'declprval' with incomplete return type 'A'

但是如果實作為右值參考的形式, 那麼就不會產生上述編碼錯誤. 另外, 如果 T 的解構子是 private 的, 那麼直接回傳 Tdeclprval 也不能用於產生 T 的右值物件

如果你很細心, 你就會發現我在實作 test_destructible 的過程中, 傳遞給 select_second_type 的第一個樣板引數是 decltype(std::declval<T &>().~T()) 而不是 decltype(std::declval<T>().~T()), 中間差了一個參考. 這是因為編碼器在解構的過程中, 只接受一個指標, 指標就必然指向一個左值. 而很顯然, std::declval 產生的是右值參考, 因此需要通過參考折疊方式令其成為左值參考以獲得一個左值

最後, std::is_swappable_withstd::is_swappable 我就不再實作了. 不過, 我需要和大家說明一下這兩個超函式的區別. std::is_swappable_with 是用於檢測兩個不同的型別是否可交換, 而 std::is_swappable 僅用於一個型別的. 另外, std::is_swappable_with 要求兩個型別 TU 之間, T 可以和 U 進行交換, U 也可以和 T 進行交換. 同時滿足之後, std::is_swappable_with 才回傳 true. 若我們使用 std::swap 進行判斷型別 TU 是否可進行交換, 那麼給定的引數必須是左值參考, 也就是說 std::is_swappable_with<T &, U &>::value 才可能回傳 true; 否則, 永遠都是 false. 因為 std::swap 要求函式引數必須是一個參考. 這對於其它要求參考型別的超函式也是一樣適用的

25. 對於運算子多載

在 C++ 中, 要檢測某個類別是否多載了運算子, 可以使用上面的方法進行檢測. 所有允許多載的運算子都可以使用相同的方式進行檢測, 此處就不再累贅了

26. std::is_nothrow_constructible, std::is_nothrow_default_constructible, std::is_nothrow_copy_constructible, std::is_nothrow_move_constructible, std::is_nothrow_assignable, std::is_nothrow_copy_assignable, std::is_nothrow_move_assignable, std::is_nothrow_swappable_with, std::is_nothrow_swappblestd::is_nothrow_convertible

要想判定某個操作是否是不擲出例外情況的, 首先需要檢測這個操作是否可以進行. 因此, 如果 is_nothrow_operable 對一個的 is_operable 的回傳結果為 fasle, 那麼就可以不需要進一步檢查, 直接回傳 false 就可以了. 這是一種思路. 但是我們又知道, 當操作不可行的時候, SFINAE 起作用, 編碼器會直接忽略掉存在不可行操作的函式匹配, 因此我們也可以直接進行實作, 不需要提前判定該操作可不可行. 為了檢測某個操作是否會擲出例外情況, 我們想到 noexcept 除了可以標識之外, 還可以作為運算子使用 :

template <typename, typename T>
struct select_second_type {
    using type = T;
};
template <typename T>
bool_constant<noexcept(T())> test_nothrow_default_constructible(int) noexcept;
template <typename T>
false_type test_nothrow_default_constructible(...) noexcept;
template <typename T>
struct is_nothrow_destructible_ : decltype(test_nothrow_default_constructible<T>(0)) {};

剩下的我就不實作了

27. std::alignment_ofsize_of

這兩個實際上就是把 alignof 和 sizeof 的結果保存下來而已 :

template <typename T>
struct size_of : constant<size_t, sizeof(T)> {};

template <typename T>
struct alignment_of : constant<size_t, alignof(T)> {};

28. std::rankstd::extent

std::rank 用於獲取陣列的維度. 這個類似於 std::remove_extents, 我們只需要在移除的同時, 統計移除的維度個數即可 :

template <typename T>
struct rank : constant<size_t, 0> {};
template <typename T>
constexpr inline auto rank_v {rank<T>::value};
template <typename T>
struct rank<T []> : constant<size_t, rank_v<T> + 1> {};
template <typename T, size_t N>
struct rank<T [N]> : constant<size_t, rank_v<T> + 1> {};

當一個型別不是陣列的時候, 它的維度是 0; 否則, 每次移除一個維度, 保存的值就是 rank_v<T> + 1. 其中, rank_v 是一個變數樣板, 使用變數樣板可以讓我們寫少一些程式碼

std::extent 接受兩個樣板引數 :

template <typename T, size_t = 0>
struct extent;

第二個引數是用於獲取陣列對應維度的大小. 對於給定的 N, 我們只需要移除前 N - 1 個維度, 然後保存第 N 維度的大小即可. std::extent 的實作可以借助 std::remove_extents :

template <typename T, size_t = 0>
struct extent : constant<size_t, 0> {};
template <typename T>
struct extent<T [], 0> : constant<size_t, 0> {};
template <typename T, size_t N>
struct extent<T [], N> : extent<T, N - 1> {};
template <typename T, size_t Size>
struct extent<T [Size], 0> : constant<size_t, Size> {};
template <typename T, size_t Size, size_t N>
struct extent<T [Size], N> : extent<T, N - 1> {};

29. std::underlying_type

這個超函式用於獲取列舉的底層型別. 一般來說, 未標識型別的列舉預設型別為 int. 而對於標識了型別的列舉, 若我們想得到其底層型別, 是不能使用 decltype 的, 因為 decltype 會得到對應列舉的型別, 因此要使用 std::underlying_type 才能獲取. 而 std::underlying_type 我們並不能實作, 也是要借助編碼器魔法 :

template <typename T>
struct underlying_type {
    using type = __underlying_type(T);
};