摘要訊息 : 實作 <type_traits>
是深入了解 C++ 樣板超編程的必經之路.
0. 前言
在文章
- 《【C++ Template Meta-Programming】認識樣板超編程 (TMP)》,
- 《【C++ Template Meta-Programming】參數列表》,
- 《【C++ Template Meta-Programming】Traits 技巧》,
- 《【C++ Template Meta-Programming】函式多載與 SFINAE 初步》,
- 《【C++ Template Meta-Programming】SFINAE》和
- 《【C++ Template Meta-Programming】到處 SFINAE》
中, 已經大致為大家介紹了 C++ 中的樣板超編程. 如果看懂上述文章都沒有什麼問題的話, 說明閣下對 C++ 樣板超編程可能已經入門了. 我認為自己實作 C++ 標準樣板程式庫中的標頭檔 <type_traits>
非常重要, 這能夠帶你更加深入地理解 C++ 樣板超編程. 所以, 這篇文章將實作一部分來自 <type_traits>
的超函式. 由於 <type_traits>
中的樣板很多, 所以我們會將文章分成兩個部分.
帶有 std::
名稱空間的是來自 C++ 標準樣板程式庫中的樣板類別, 不帶有名稱空間的是我自己認為需要加入到這篇文章中的.
我自己實作的 <type_traits>
在 https://github.com/Jonny0201/data_structure/blob/master/data_structure/type_traits.hpp 中可以找到.
更新紀錄 :
- 2022 年 5 月 31 日進行第一次更新和修正.
1. 常數
常數系列是指在樣板超編程中扮演常數的一些類別, 例如我們可能會用 number<20>
來替換純數字 20
.
1.1 constant
由於 C++ 20 引入了類別樣板參數 (P0732R2), 所以樣板現在不僅僅可以接收內建型別常數, 也可以接受用戶自訂類別的常數. 所以, 類別樣板 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;
}
};
1.2 std::integral_constant
std::integral_constant
實際上是儲存整型型別常數的, 所以它和 constant
差不多 :
template <typename T, T Value>
struct integral_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;
}
};
當然, 我們在 C++ 20 中也可以借助 constant
配合 requires
來實作 :
#include <type_traits>
template <typename T, T Value> requires std::is_integral_v<T>
using integral_constant = constant<T, Value>;
1.3 布林常數
bool
型別有且未有兩個值 : true
和 false
, 所以一般來說我們都會特意列舉這兩個值. 對應地, <type_traits>
中就列舉了 std::true_type
和 std::false_type
, 這兩個都是 std::bool_constant
的特製化.
template <bool Value>
using bool_constant = constant<bool, Value>;
template <>
using true_type = bool_constant<true>;
template <>
using false_type = bool_constant<false>;
值得注意的是, std::true_type
和 std::false_type
在樣板超編程中有著非常重要的作用, 很多類別樣板都可能是從這兩個類別繼承的.
利用 C++ 14 引入的變數樣板 (《【C++ 14】變數樣板》), 我們可以實作 make_true
和 make_false
:
template <typename ...>
constexpr inline bool make_true {true};
template <typename ...>
constexpr inline bool make_false {false};
2. 條件
樣板超編程中條件判斷非常重要, 因此必須實作一些能夠配合特製化, 偏特製化和 SFINAE 的條件超函式.
2.1 std::conditional
template <bool, typename If, typename>
struct conditional {
using type = If;
};
template <typename If, typename Then>
struct conditional<false, If, Then> {
using type = Then;
};
2.2 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;
};
3. 型別
樣板超編程中存在不少和型別變換有關的超函式. 除了添加一些標識和移除一些標識之外, 我們還會實作型別容器等.
3.1 make_type
我們在文章《【C++ Template Meta-Programming】到處 SFINAE》中已經實作過 make_void
, 它也通常被用於 SFINAE. 對於 make_void
, 其功能和 std::void_t
一樣, 只不過它是一個類別樣板, 而 std::void_t
是一個型別別名. 我們實作更加通用的 make_type
:
template <typename T, typename ...>
struct make_type {
using type = T;
};
為了加快編碼速度, 我們不通過 make_type
來實作 make_void
, make_true_type
或者 make_false_type
:
template <typename ...>
struct make_void {
using type = void;
};
template <typename ...>
struct make_true_type {
using type = true_type;
};
template <typename ...>
struct make_false_type {
using type = false_type;
};
std::void_t
是一個型別別名, 所以可以直接借助 using
進行宣告 :
template <typename ...>
using void_t = void;
3.2 std::type_identity
和 type_holder
這兩個類別樣板的作用相同, 都是用於持有某一個型別 :
template <typename T>
struct type_identity {
using type = T;
};
template <typename T>
using type_holder = type_identity<T>;
3.3 type_container
type_container
是一個型別容器, 用於持有一系列的型別, 就像 std::vector
持有若干個元素一樣. 我們一般使用 C++ 11 引入的可變樣板參數來實作. 我們要考慮的有 :
- 如何獲取當前的型別
T
; - 如何獲取除了
T
之外, 位於型別容器中的剩餘型別; - 如若型別容器僅持有一個型別, 那麼剩餘型別應該如何表示;
- 如果沒有給定樣板引數, 應該如何處理.
獲取當前型別, 這實際上就是超函式的回傳值, 實作和上面幾個類別一樣. 對於剩餘的型別, 我們可以把其重新放進 type_container
中, 然後用一個型別別名代表它. 實際上, 這就是超函式有了兩個回傳值. 對於沒有給定樣板引數的 type_container
, 它一般會被當作迴圈結束標誌, 我們要求結束標誌必須是獨一無二. 我們可以宣告一個新的類別, 例如 struct unique;
, 然後把它用於結束標誌. 但是可變參數樣板允許樣板引數為空, 而且當樣板引數為空的時候, 它和任意其它型別都不同. 因此我們可以直接使用 type_container<>
作為結束標誌.
template <typename ...>
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 ...Ts>
struct type_container<T, Ts...> {
using type = T;
using remaining = type_container<Ts...>;
};
3.4 unique_type
編碼器會為每一個 Lambda 表達式生成一個獨一無二的類別. 因此, 我們可以將其用於樣板參數來生成獨一無二的類別 :
template <void () = [] {}>
struct unique_type {};
我們可以通過 static_assert(std::is_same_v<unique_type<>, unique_type<>>, "The type is not same!");
來測試 unique_type
是否是真的獨一無二的型別. 然而, 靜態斷言永遠不會通過, 永遠都會產生編碼錯誤.
3.5 添加 const
和 volatile
為某個型別添加 const
或者 volatile
限定比較簡單, 因為對於任何型別都可以添加 const
和 volatile
限定, 而且當在某個出現多個 const
或者 volatile
時, 編碼器會幫助我們去處多餘的 const
和 volatile
, 而不會產生編碼錯誤. std::add_cv
是為某個型別同時添加 const
和 volatile
限定符.
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;
};
3.6 添加參考和指標標識
我們不能像第 3.5 節那樣直接通過添加參考或者指標標識來實作 std::add_lvalue_reference
, std::add_rvalue_reference
, 因為像 void
型別就不存在參考. 另外, 針對函式型別來說, 如果其帶有 const
, volatile
或者參考限定, 那麼這個函式型別也不能再為其添加參考限定, 否則會導致編碼錯誤. 對於指標來說, 不存在參考的指標.
對於部分型別, 當為其添加指標或者參考失敗的時候, 我們不希望產生編碼錯誤, 而是希望最終回傳結果是原來的型別. 那麼我們想到使用 SFINAE, 同時借助 decltype
推導函式的回傳型別. 這裡, 我僅寫出 std::add_lvalue_reference
的實作, 剩餘的 std::add_rvalue_reference
和 std::add_pointer
大家可以自行實作 :
template <typename T>
T &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));
};
add_const_reference
和 add_const_pointer
是組合的超函式. 這裡要特別拿出來是因為需要注意添加的順序, 順序不正確會導致結果不正確. 針對 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
限定的左值參考. 除此之外, 添加參考時, 也需要注意順序問題. 具體的實作和上面一樣, 這裡我不再累贅.
3.7 移除標識
樣板在偏特製化的時候, 會針對給定的型別匹配到對應的偏特製化後的類別中. 利用這個特性, 我們可以實作 std::remove_lvalue_reference
(剩餘的 std::remove_rvalue_reference
, std::remove_pointer
, std::remove_reference
, std::remove_const
和 std::remove_cv
) :
template <typename T>
struct remove_lvalue_reference {
using type = T;
};
template <typename T>
struct remove_lvalue_reference<T &> {
using type = T;
};
我們同樣指出 remove_const_reference
是移除帶有 const
標識的左值參考, 而不是右值參考. 同時, 也要注意移除的順序, 首先移除參考標識, 然後才移除 const
標識. C++ 20 引入了 std::remove_cvref
, 它會移除任意參考標識, 然後再移除 const
和 volatile
標識. 這裡我們僅實作 remove_const_reference
, std::remove_cvref
的實作大家可以自行嘗試 :
template <typename T>
struct remove_const_reference : remove_const<typename remove_reference<T>::type> {};
這裡使用了繼承, 這是為了寫少了一些程式碼, 最終的效果是一樣的.
3.8 移除陣列維度
std::remove_extent
和 remove_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 {
using type = T;
};
template <typename T>
struct remove_extents<T []> {
using type = typename remove_extents<T>::type;
};
template <typename T, size_t N>
struct remove_extents<T [N]> {
using type = typename remove_extents<T>::type;
};
4. 判斷
在樣板超編程的時候, 我們通常需要判斷某一些型別是否符合要求, 就可以借助這些判斷的超函式. 這些超函式內部都帶有一個 bool
型別的靜態成員變數, 表明結果是 true
還是 false
. 因此, 我們直接讓這些類別繼承 std::true_type
或者 std::false_type
即可. 不需要重新去寫一個類似於 constant
的類別.
4.1 std::is_same
template <typename, typename>
struct is_same : false_type {};
template <typename T>
struct is_same<T, T> : true_type {};
4.2 標識判斷
std::is_const
, std::is_volatile
, is_cv
, std::is_lvalue_reference
, std::is_rvalue_reference
, std::is_reference
和 std::is_pointer
用於判斷是否存在某個標識, 這裡僅僅實作 std::is_const
:
template <typename T>
struct is_const : false_type {};
template <typename T>
struct is_const<const T> : true_type {};
4.3 std::is_void
和 is_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>
struct is_null_pointer : is_same<decltype(nullptr), typename remove_cv<T>::type> {};
4.4 數值型別判斷
std::is_integral
, std::is_unsigned
, std::is_signed
, is_character
和 std::is_floating_point
這幾個超函式都有比較多的型別需要特製化. 和 std::is_void
與 is_null_pointer
相同, 這幾個函式同樣需要移除 const
和 volatile
限定. C++ 中屬於整型的型別有 : bool
, char
, signed char
, unsigned char
, wchar_t
, char16_t
, char32_t
, char8_t
(since C++ 20), short
, unsigned short
, int
, unsigned int
, long
, unsigned long
, long long
和 unsigned long long
. 某些編碼器可能會內建一個 128 位元的整型型別 : __int128_t
和 __uint128_t
. 這裡要特別注意, signed char
和 char
是屬於不同的型別, unsigned char
和 char
也是屬於不同的型別. C++ 中屬於浮點數型別的有 : float
, double
和 long 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> {};
4.5 陣列型別判斷
陣列型別不需要考慮其是否帶有 const
, volatile
或者參考限定, 因此 std::is_array
實作比較簡單 :
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 {};
C++ 20 還引入了 std::is_bounded_array
和 std::is_unbounded_array
, 用於判斷陣列的維度大小是否給出 :
template <typename>
struct is_bounded_array : false_type {};
template <typename T, size_t N>
struct is_bounded_array<T [N]> : true_type {};
template <typename>
struct is_unbounded_array : false_type {};
template <typename T, size_t N>
struct is_unbounded_array<T []> : true_type {};
4.6 is_type
對於任意型別, is_type
都回傳 true
. 它一般用於樣板中的延時計算, 這涉及到樣板的二階段名稱查找, 此處暫時不具體展開.
template <typename T>
struct is_type : true_type {};
4.7 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_type
的 test_complete
函式版本.
4.8 std::is_function
我們一般講到函式型別, 一般都會聯想到函式指標. 但是當我們將函式指標放入 std::remove_pointer
中之後, 得到的是一個完完全全的函式型別. 大家可能對函式型別非常陌生, 因為幾乎用不到函式型別. 在 C++ 中, 函式型別和陣列型別一樣, 具有衰退的特性, 可以隱含地轉變為函式指標型別. 例如 void func(int []);
和 void func(int *);
是一樣的宣告, 而 void func(void (*)());
和 void func(void ());
也是一樣的宣告.
然而函式的型別非常複雜. 要判定一個型別是否為函式型別, 首先要知道它的參數列表. 在樣板超編程中, 我們甚至沒有辦法知道給定的函式型別的參數列表中有多少參數, 參數分別是什麼型別. 因此, 我們需要借助 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 風格的可變參數列表, 即函式參數最後是以省略號結尾的. 上面的可變參數樣板只能對存在於參數列表中, 省略號之前的參數型別進行推導, 無法針對省略號中的參數型別進行推導. 但是, 我們其實可以直接將省略號用於函式型別中 :
template <typename R, typename ...Args>
struct is_function<R (Args..., ...)> : true_type {};
當然, 如果把樣板特製化列表中的 R(Args..., ...)
改為 R(Args......)
效果是一樣的, 但是在 Apple Clang 下會產生編碼警告.
另外, 在 C++ 17 之後 noexcept
會影響函式的型別, 因此還需要針對帶有 noexcept
的函式型別進行偏特製化 :
template <typename R, typename ...Args>
struct is_function<R (Args...) noexcept> : true_type {};
對於類別的成員函式, 其還可以帶有 const
, volatile
和參考限定, 因此我們還需要對這些進行偏特製化. 這裡僅給出一種實例 :
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 {};
4.9 成員指標型別判斷
對於指向成員的指標, 它們雖然都是指標型別, 但是它有著特殊的寫法 : 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_function_pointer : is_member_function_pointer_auxiliary<typename remove_cv<T>::type> {};
對於指向成員物件的指標型別來說, 首先它必須是指標, 其次只要它不是指向成員函式的, 那麼就必定是指向成員物件的 :
template <typename T>
struct is_member_object_pointer : bool_constant<is_pointer<T>::value and not is_member_function_pointer<T>::value> {};
4.10 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)> {};
4.11 建構, 指派, 解構, 轉型和交換
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_convertible
, std::is_swappable_with
和 std::is_swappable
這些超函式是和型別的建構, 指派, 解構和交換有關的. 一般來說, 它們可以用於判斷對與一個型別來說, 是否可以
- 進行預設建構;
- 進行解構;
- 由給定的型別進行建構;
- 進行複製建構;
- 被給定的型別指派;
- 進行複製指派;
- 進行移動建構;
- 進行移動指派;
- 和給定的型別進行交換.
這些超函式的實作需要借助 decltype
和 SFINAE, 同時還需要產生對應型別的一個臨時值. 例如我們要測試是否可以由一個 B &
型別的值對 A
型別的值進行指派, 即類似於 A {} = B & {}
這樣的指派, 我們必須要產生一個 A
型別的臨時值和 B &
型別的臨時值. 因此我們還需要實作 std::declval
. std::declval
是一個函式宣告, 一般來說它不會被定義, 而且是不允許被呼叫的, 就像 Code 14 中的 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> {};
剩下的大家可以自行實作. 對於指派來說, 我們可以把 Code 33-1 中的 decltype(T(std::declval<Args>()...)
改為 decltype(std::declval<T>() = std::declval<U>())
, 就有能力測試指派操作了.
其實我們已經有能力判定任何函式的呼叫了. 比如我要檢測是否存在一個名為 func
的成員函式, 我們就寫為 decltype(std::declval<T>().func(std::declval<Args>()...))
. 所以對於解構子是否存在和是否可交換的判定, 我們使用類似的方法進行實作即可 :
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 : typename conditional<
not is_function<T>::value and not is_unbounded_array<T>::value and not is_same<void, T>::value,
typename conditional<
is_reference<T>::value,
true_type,
typename conditional<
is_complete<T>::value,
decltype(test_destructible<typename remove_extents<T>::type>(0)),
false_type
>::type
>::type,
false_type
>::type {};
除此之外, 我們甚至可以寫 is_list_constructible
, is_static_castable
, is_dynamic_castable
, is_const_castable
, is_reinterpret_castable
和 is_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; // Error : calling 'declprval' with incomplete return type 'A'
如果實作為右值參考的形式, 那麼 Code 39 就不會產生編碼錯誤. 另外, 如果 T
的解構子是 private
的, 那麼直接回傳 T
的 declprval
也不能用於產生 T
的右值物件.
如果你很細心, 你就會發現我在實作 test_destructible
的過程中, 傳遞給 select_second_type
的第一個樣板引數是 decltype(std::declval<T &>().~T())
而不是 decltype(std::declval<T>().~T())
, 中間差了一個參考. 這是因為編碼器在解構的過程中, 只接受一個指標, 指標就必然指向一個左值. 而很顯然, std::declval
產生的是右值參考, 因此需要通過參考折疊方式令其成為左值參考以獲得一個左值.
std::is_swappable_with
和 std::is_swappable
我就不再實作了. 不過, 我需要和大家說明一下這兩個超函式的區別. std::is_swappable_with
是用於檢測兩個不同的型別是否可交換, 而 std::is_swappable
僅用於一個型別的. 另外, std::is_swappable_with
要求兩個型別 T
和 U
之間, T
可以和 U
進行交換, U
也可以和 T
進行交換. 同時滿足之後, std::is_swappable_with
才回傳 true
. 若我們使用 std::swap
進行判斷型別 T
和 U
是否可進行交換, 那麼給定的引數必須是左值參考, 也就是說 std::is_swappable_with<T &, U &>::value
才可能回傳 true
; 否則, 永遠都是 false
. 因為 std::swap
要求函式引數必須是一個參考. 這對於其它要求參考型別的超函式也是一樣適用的.
最後, 我們還可以通過配合 decltype
, std::declval
和 SFINAE 來實作判斷運算子是否多載的函式. 這些也留給大家自行實作
4.12 例外情況
要想判定某個操作是否是不擲出例外情況的, 首先需要檢測這個操作是否可以進行. 因此, 如果 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)) {};
對第 4.11 節中提到的所有判斷, 都可以增加一個例外情況的額外判斷, 這些留給大家自行實作.
5. 計算
有時候, 我們需要對型別進行一些計算.
5.1 std::alignment_of
和 size_of
這兩個實際上就是把 alignof
(《【C++】記憶體對位》) 和 sizeof
的結果保存下來而已 :
template <typename T>
struct size_of : constant<size_t, sizeof(T)> {};
template <typename T>
struct alignment_of : constant<size_t, alignof(T)> {};
5.2 std::rank
和 std::extent
std::rank
用於獲取陣列的維度. 這個類似於 std::remove_extents
, 我們只需要在移除的同時, 統計移除的維度個數即可 :
template <typename T>
struct rank : constant<std::size_t, 0> {};
template <typename T>
struct rank<T []> : constant<std::size_t, rank<T>::value + 1> {};
template <typename T, size_t N>
struct rank<T [N]> : constant<std::size_t, rank<T>::value + 1> {};
當一個型別不是陣列的時候, 它的維度是 0
; 否則, 每次移除一個維度, 保存的值就是 rank_v<T> + 1
.
std::extent
接受兩個樣板引數, 第一個引數是要計算的陣列型別, 第二個引數是用於獲取陣列對應維度的大小. 對於給定的 N
, 我們只需要移除前 N - 1
個維度, 然後保存第 N
維度的大小即可 :
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> {};
6. 編碼器魔法
<type_traits>
中存在不少超函式是我們自己沒有辦法實作的, 必須借助編碼器為我們提供的黑魔法. 這些黑魔法看起來像是一個關鍵字, 就像 sizeof
那樣.
6.1 判斷
有些類別從名字可能無法猜到其具體的用途, 這裡我解釋一下
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
:- 陣列型別;
- 對於類別來說, 至少滿足 :
- 不存在
private
及protected
的非靜態成員; - 不存在用戶自訂或者繼承的建構子;
- 不存在虛擬基礎類別以及私用繼承的基礎類別;
- 不存在虛擬成員函式;
- 不存在
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 型別;
- 陣列型別.
這裡我只實作 std::is_eunm
作為範例 :
template <typename T>
struct is_enum : bool_constant<__is_enum(T)> {};
可以看到, 我們只需要在名稱前面加兩個下劃線, 然後和 sizeof
差不多的形式去使用它們, 參數列表是一些型別, 就可以獲得一個 bool
型別的結果. 由於有了這個編碼器魔法之後, 實作變得特別簡單, 因此剩餘的超函式實作我不再累贅.
值得注意的是, 針對有些我們可以自行完成的超函式 : std::is_convertible
, std::is_constructible
, std::is_assignable
, std::is_nothrow_constructible
和 std::is_nothrow_assignable
, 編碼器也為我們提供了上面的魔法. 不過既然我們可以自行完成, 我也會在下面進行講解. 需要注意的是, 對於 std::is_convertible
, Apple Clang 提供的魔法版本的名稱為 __is_convertible_to
.
另外, 由於這些魔法是編碼器自訂的, 屬於實作定義行為, 所以不同編碼器提供的魔法名稱可能稍有不同.
6.2 std::underlying_type
這個超函式用於獲取列舉的底層型別. 一般來說, 未標識型別的列舉預設型別為 int
. 而對於標識了型別的列舉, 若我們想得到其底層型別, 是不能使用 decltype
的, 因為 decltype
會得到對應列舉的型別, 因此要使用 std::underlying_type
才能獲取. 而 std::underlying_type
我們並不能實作, 也是要借助編碼器魔法 :
template <typename T>
struct underlying_type {
using type = __underlying_type(T);
};
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :