摘要訊息 : 實作 <type_traits>
是深入了解 C++ 樣板超編程的必經之路.
0. 前言
在文章《【C++】實作 <type_traits>
(上)》中, 我們實作了 <type_triats>
中比較簡單的那一部分. 在本篇文章中, 我們將攻克 <type_traits>
相對較難的部分.
帶有 std::
名稱空間的是來自 C++ 標準樣板程式庫中的樣板類別, 不帶有名稱空間的是我自己認為需要加入到這篇文章中的. 另外為了方便起見, 我將運用樣板別名和樣板引數. 在 C++ 14 之後, 標頭檔 <type_traits>
引入了以 _t
和 _v
結尾的別名. 其中 _t
後綴表示樣板別名, _v
後綴表示樣板引數. 舉個例子, 對於 std::add_pointer
, 一般對於型別 T
, 我們要獲取其增加一個指標之後的型別, 要寫成 typename std::add_pointer<T>::type
. 使用了帶有 _t
後綴的樣板別名之後, 就可以寫成 std::add_pointer_t<T>
對於 std::is_same
, 要想判定型別 T
和 U
是否一致, 要寫成 std::is_same<T, U>::value
. 使用了帶有 _v
後綴的樣板引數之後, 我們只要寫成 std::is_same_v<T, U>
即可.
部分已經在《【C++】實作 <type_traits>
(上)》中實作過的超函式或者輔助函式我將省略掉名稱空間.
我自己實作的 <type_traits>
在 https://github.com/Jonny0201/data_structure/blob/master/data_structure/type_traits.hpp 中可以找到.
更新紀錄 :
- 2022 年 6 月 5 日進行第一次更新和修正.
1. std::decay
std::decay
的作用是對於任意給定的型別引數, 去除其 const
標識, volatile
標識和參考標識. 除此之外, 它還可以讓函式型別退化為函式指標型別, 讓陣列型別退化為陣列指標型別 :
template <typename T>
struct decay {
using type = conditional_t<
is_array_v<remove_reference_t<T>>,
add_pointer_t<remove_extent_t<remove_reference_t<T>>>,
conditional_t<is_function_v<remove_reference_t<T>>, add_pointer_t<remove_reference_t<T>>, remove_cv_t<remove_reference_t<T>>>
>;
};
2. 函式呼叫
std::result_of
, std::invoke_result
, std::is_invocable
, std::is_nothrow_invocable
, std::is_invocable_r
和 std::is_nothrow_invocable_r
都是和函式呼叫有關的超函式. std::result_of
和 std::invoke_result
負責回傳呼叫的結果, std::is_invocable
負責檢查是否可以呼叫, std::is_invocable_r
負責檢查呼叫的結果是否可以向某個型別轉型. 其中, std::result_of
在 C++ 17 中被遺棄, 在 C++ 20 中被移除.
#include <type_traits>
#include <typeinfo>
struct callable {
void *operator()(int, char, decltype(typeid(int))) const noexcept {
return nullptr;
}
};
static_assert(std::is_same_v<std::result_of_t< /* 注意這裏和下面的區別 */ callable (int, char, decltype(typeid(int)))>, void *>); // OK
static_assert(std::is_same_v<std::invoke_result_t<callable, int, char, decltype(typeid(int))>, void *>); // OK
這裏要注意的是, 可呼叫物件不一定指的是函式, 對成員變數的存取也可以當作可呼叫物件的呼叫, 其回傳型別是對應成員變數的型別. 因此, 只要所有給定的型別都是已經定義的完整型別, 甚至未給定邊界的陣列型別和 void
系型別, 都可以應用這兩個超函式. 但是, 若給定的引數是不可呼叫的, 那麼會產生編碼錯誤.
我們可以運用 SFINAE, 針對成員函式的呼叫、成員變數的存取和普通函式的呼叫進行實作. 首先我們需要定義一個表示呼叫失敗的標誌型別 :
struct invoke_failure_tag {};
另外, C++ 標準樣板程式庫中 std::invoke_result
和 std::result_of
都考慮了 std::reference_wrapper
的情況. 此處, 我們暫時不考慮. 我們首先實作成員變數函式的兩種情況. 對成員函式的呼叫, 除了可以通過 ".
" 運算子之外, 還可以通過成員函式指標配合 ".*
" 運算子進行, 因此我們要將兩種情況都納入其中 :
/* 針對一般形式 */
template <typename Fp, typename T, typename ...Args>
static constexpr decltype((declval<T>().*declval<Fp>())(declval<Args>()...)) test_member_function(int) noexcept;
template <typename ...>
static constexpr invoke_failure_tag test_member_function(...) noexcept;
template <typename Fp, typename T, typename ...Args>
static consteval inline bool is_nothrow_member_function() noexcept {
return noexcept((declval<T>().*declval<Fp>())(declval<Args>()...));
}
/* 針對成員函式指標形式 */
template <typename Fp, typename Ptr, typename ...Args>
static constexpr decltype((declval<Ptr>()->*declval<Fp>())(declval<Args>()...)) test_member_function_deref(int) noexcept;
template <typename ...>
static constexpr invoke_failure_tag test_member_function_deref(...) noexcept;
template <typename Fp, typename Ptr, typename ...Args>
static consteval inline bool is_nothrow_member_function_deref() noexcept {
return noexcept((declval<Ptr>()->*declval<Fp>())(declval<Args>()...));
}
其中, 名稱中帶有 nothrow
的函式使用了 noexcept
運算子測試了呼叫是否不擲出任何例外情況. 這是為了 std::is_nothrow_invocable
和 std::is_nothrow_invocable_r
準備的. 上面的兩個部分都是為通過類別物件呼叫成員函式準備的, 所以我們可以看到必須使用 std::declval
對類別產生對應的物件.
與成員函式的呼叫類似, 我們可以實作出針對成員物件存取的測試函式 :
/* 針對一般形式 */
template <typename Op, typename T>
static constexpr decltype(declval<T>().*declval<Op>()) test_member_object(int) noexcept;
template <typename, typename>
static constexpr invoke_failure_tag test_member_object(...) noexcept;
template <typename Op, typename T>
static consteval inline bool is_nothrow_member_object() noexcept {
return noexcept(declval<T>().*declval<Op>());
}
/* 針對成員變數指標形式 */
template <typename Op, typename Ptr>
static constexpr decltype(declval<Ptr>->*declval<Op>()) test_member_object_deref(int) noexcept;
template <typename, typename>
static constexpr invoke_failure_tag test_member_object_deref(...) noexcept;
template <typename Op, typename Ptr>
static consteval inline bool is_nothrow_member_object_deref() noexcept {
return noexcept(declval<Ptr>()->*declval<Op>());
}
而針對普通函式的測試函式非常簡單, 因為一般的可呼叫物件直接通過函式呼叫運算子就可以呼叫 :
template <typename F, typename ...Args>
static constexpr decltype(declval<F>()(declval<Args>()...)) test_function(int) noexcept;
template <typename ...>
static constexpr invoke_failure_tag test_function(...) noexcept;
template <typename F, typename ...Args>
static consteval inline bool is_nothrow_function() noexcept {
return noexcept(declval<F>()(declval<Args>()...));
}
得到這些測試函式之後, 我們需要通過超函式獲得呼叫結果對應的型別. 在此之前, 我們宣告一個未實作的類別 :
template <typename, typename>
struct result_of_member_object;
第一個樣板引數接受成員指標的型別, 第二個引數接受要測試存取的型別. 接下來可以針對這個樣板特製化 :
template <typename PreT, typename Class, typename T>
struct result_of_member_object<PreT Class::*, T> {
using type = conditional_t<
is_same_v<remove_cvref_t<T>, Class> or is_base_of_v<Class, remove_cvref_t<T>>,
decltype(test_member_object<PreT Class::*, T>(0)),
decltype(test_member_object_deref<PreT Class::*, T>(0))>;
};
一般來說, 若我們要測試某個類別中是否存在可存取的 T
型別的成員, 是需要將繼承考慮進來的. 如果型別 T
是給定引數型別 Class
的衍生類別, 或者和型別 Class
相同, 那麼是通過物件進行訪問的; 否則, 就是通過成員物件指標的形式來訪問的. 以此, 我們來區分兩者是否使用帶有解參考的那個測試函式. 這個超函式的使用方法為, 對於給定的成員函式指標 PreT Class::*
, 是否可以通過型別 T
來訪問這個成員. 當 T
不是指標的時候, 其訪問方法為 std::declval<T>().*std::declval<PreT Class::*>()
; 如果 T
是指標型別, 那麼訪問方法為 std::declval<T>()->*std::declval<PreT Class::*>()
. 最後, 對於給定的型別引數, 我們還看到要去除其參考標識, const
標識和 volatile
標識.
有了這個經驗之後, 我們很容易就實作出 result_of_member_function
, result_of_nothrow_object
和 result_of_nothrow_function
:
template <typename, typename>
struct result_of_nothrow_member_object;
template <typename PreT, typename Class, typename T>
struct result_of_nothrow_member_object<PreT Class::*, T> :
conditional_t<is_same_v<remove_cvref_t<T>, Class> or is_base_of_v<Class, remove_cvref_t<T>>,
bool_constant<is_nothrow_member_object<PreT Class::*, T>()>,
bool_constant<is_nothrow_member_object_deref<PreT Class::*, T>()>> {};
template <typename ...>
struct result_of_member_function;
template <typename PreT, typename Class, typename Fp, typename ...Args>
struct result_of_member_function<PreT Class::*, Fp, Args...> {
using type = conditional_t<
is_same_v<remove_cvref_t<Fp>, Class> or is_base_of_v<Class, remove_cvref_t<Fp>>,
decltype(test_member_function<PreT Class::*, Fp, Args...>(0)),
decltype(test_member_function_deref<PreT Class::*, Fp, Args...>(0))>;
};
template <typename ...>
struct result_of_nothrow_member_function;
template <typename PreT, typename Class, typename Fp, typename ...Args>
struct result_of_nothrow_member_function<PreT Class::*, Fp, Args...> :
conditional_t<is_same_v<remove_cvref_t<Fp>, Class> or is_base_of_v<Class, remove_cvref_t<Fp>>,
bool_constant<is_nothrow_member_function<PreT Class::*, Fp, Args...>()>,
bool_constant<is_nothrow_member_function_deref<PreT Class::*, Fp, Args...>()>> {};
對於成員函式和成員變數的處理, 我們都已經寫好了, 接下來我們需要把它們整合起來 :
template <bool, bool, typename, typename ...>
struct result_of_auxiliary : invoke_failure_tag {};
其中, 第一個給定的樣板引數表示是否存取成員物件, 第二個給定的樣板引數表示是否存取成員函式. 如果兩個都為 false
, 那麼就是呼叫普通的可呼叫物件.
template <typename MenPtr, typename T>
struct result_of_auxiliary<true, false, MenPtr, T> : conditional_t<
is_same_v<invoke_failure_tag, typename result_of_member_object<decay_t<MenPtr>, T>::type>,
invoke_failure_tag,
result_of_member_object<decay_t<MenPtr>, T>> {};
template <typename MenPtr, typename T, typename ...Args>
struct result_of_auxiliary<false, true, MenPtr, T, Args...> : conditional_t<
is_same_v<invoke_failure_tag, typename result_of_member_function<decay_t<MenPtr>, T, Args...>::type>,
invoke_failure_tag,
result_of_member_function<decay_t<MenPtr>, T, Args...>> {};
template <typename F, typename ...Args>
struct result_of_auxiliary<false, false, F, Args...> : conditional_t<
is_same_v<invoke_failure_tag, decltype(test_function<F, Args...>(0))>,
invoke_failure_tag,
type_identity<decltype(test_function<F, Args...>(0))>> {};
對於成員變數存取, 給定的型別 T
來說, 我們還要解除其覆蓋, 即 unwrap
, 但是此處我們不考慮這種情況. 如果要考慮的話, 只需要實作 unwrap
超函式, 然後將 T
替換為 typename unwrap<T>::type
即可. 我們還注意到這裡使用了 std::decay
, 這是因為給定的引數很可能帶有各種標識, 而 result_of_member_object
和 result_of_member_function
並沒有考慮到這些標識的情況, 因此此處要移除.
對於 result_of_nothrow_auxiliary
也是類似, 但是它不是預設繼承自 invoke_failure_tag
, 而是繼承自 std::false_type
. 因為是否為不擲出例外情況, 只有 std::true_type
和 std::false_type
才能表達 :
template <bool, bool, typename ...>
struct result_of_nothrow_auxiliary : false_type {};
模仿 result_of_auxiliary
, 我們可以實作出 :
template <typename MemPtr, typename T>
struct result_of_nothrow_auxiliary<true, false, MemPtr, T> :
result_of_nothrow_member_object<decay_t<MemPtr>, T> {};
template <typename MemPtr, typename T, typename ...Args>
struct result_of_nothrow_auxiliary<false, true, MemPtr, T, Args...> :
result_of_nothrow_member_function<decay_t<MemPtr>, T> {};
template <typename F, typename ...Args>
struct result_of_nothrow_auxiliary<false, false, F, Args...> :
bool_constant<is_nothrow_function<F, Args...>()> {};
最終在上面這些輔助超函式的幫助下, 我們可以實作出 std::invoke_result
和 std::result_of
:
template <typename Ptr, typename ...Args>
struct invoke_result : result_of_auxiliary<is_member_object_pointer_v<remove_reference_t<Ptr>>,
is_member_function_pointer_v<remove_reference_t<Ptr>>, Ptr, Args...> {
static_assert(is_complete_v<Ptr>, "The first template argument must be a complete type!");
};
template <typename ...>
struct result_of;
template <typename F, typename ...Args>
struct result_of<F (Args...)> : invoke_result<F, Args...> {};
上面我們在實作的時候, result_of_auxiliary
繼承了 invoke_failure_tag
, 這是為了實作 std::is_invocable
準備的. 要判定是否可以呼叫, 我們只需要再加一個輔助性的類別 :
template <typename, typename = void>
struct is_invocable_auxiliary : false_type {};
template <typename Result>
struct is_invocable_auxiliary<Result, void_t<typename Result::type>> : true_type {};
通過上面的實作, 我們注意到, 對於可以呼叫的時候, invoke_result
中存在一個名稱為 type
的回傳型別; 但是如果不能呼叫的時候, 這個名稱為 type
的型別別名就不會存在於 std::invoke_result
中, 因此我們在樣板偏特製化中運用 SFINAE, 達到檢測的目的, 便可以實作 std::is_invocable
:
template <typename F, typename ...Args>
struct is_invocable : is_invocable_auxiliary<invoke_result<F, Args...>> {};
std::is_invocable_r
除了用於檢測呼叫之外, 還用於檢測回傳型別是否可以向另外一個型別發生隱含型別轉化 :
template <typename R, typename F, typename ...Args>
struct is_invocable_r : conditional_t<is_invocable_v<F, Args...>,
conditional_t<is_convertible_v<typename invoke_result<F, Args...>::type, R>, true_type, false_type>,
false_type> {};
最後對於 std::is_nothrow_invocable
和 std::is_nothrow_invocable_r
, 唯一需要注意的地方是 : std::is_nothrow_invocable_r
還要求隱含型別轉化是不擲出例外情況的 :
template <typename T>
static constexpr true_type test_convertible(T) noexcept;
static constexpr false_type test_convertible(...) noexcept;
template <typename T>
static constexpr void test_noexcept(T) noexcept;
template <typename T>
static constexpr bool_constant<noexcept(test_noexcept<T>())> test_nothrow_convertible() noexcept;
template <typename Ptr, typename ...Args>
struct is_nothrow_invocable : result_of_nothrow_auxiliary<
is_member_object_pointer_v<remove_reference_t<Ptr>>,
is_member_function_pointer_v<remove_reference_t<Ptr>>, Ptr, Args...> {};
template <typename From, typename To>
struct is_nothrow_invocable_r : conditional_t<
is_void_v<From> and is_void_v<To>,
true_type,
conditional_t<is_convertible_v<From, To>, decltype(test_nothrow_convertible<To>(declval<From>())), false_type>> {};
3. 型別適配
本節中我們將實作 std::make_signed
, std::make_unsigned
, integral_promotion
, floating_promotion
, character_promotion
和 arithmetic_promotion
. 其中, std::make_signed
和 std::make_unsigned
是使得給定的樣板引數適配至和其 sizeof
值最接近的有號數整型型別或者無號數整型型別; integral_promotion
, floating_promotion
, character_promotion
和 arithmetic_promotion
是將給定的型別提升到 sizeof
值更大的同類型型別.
要實作這些超函式, 我們需要借助文章《【C++】實作 <type_traits>
(上)》中的 type_container
. 首先, 我們把整型和浮點數型別根據其本身的屬性使用 type_container
進行收集 :
using signed_integral = type_container<signed char, signed short, signed int, signed long, signed long long, __int128_t>;
using unsigned_integral = type_container<unsigned char, unsigned short, unsigned int, unsigned long, unsigned long long, __uint128_t>;
using floating_point = type_container<float, double, long double>;
using character = type_container<char8_t, char16_t, char32_t>;
注意, character
中我們遺棄了 char
, unsigned char
, signed char
和 wchar_t
, 這是因為它只用於 character_promotion
, 而這些被遺棄的型別在這裡沒有太大用處, 我們統一為 UTF 字元型別.
接下來我們實作 std::make_unsigned
和 std::make_signed
的輔助超函式 :
template <typename T, typename Result, typename Container>
struct make_signed_or_unsigned_auxiliary {
private:
using next_type = typename Container::type;
using remaining_type = typename Container::remaining;
public:
using type = conditional_t<
sizeof(T) == sizeof(Result),
Result,
conditional_t<
(sizeof(T) > sizeof(Result)) and sizeof(T) <= sizeof(next_type),
next_type,
typename make_signed_or_unsigned_auxiliary<T, next_type, remaining_type>::type
>>;
};
template <typename T, typename Result>
struct make_signed_or_unsigned_auxiliary<T, Result, type_container<>> {
using type = Result;
};
理解起來非常簡單, 首先針對空的型別容器, 直接將 Result
作為回傳型別, 因為 Result
必定是這一類型別中 sizeof
值最大的那個, 再大就沒有內建型別能夠匹配了. 除此之外, 這還是超函式結束迴圈的標誌. 對於一般情況, 我們首先判斷給定的樣板引數 T
和 Result
的 sizeof
值是否相等. 如果相等的話, 直接回傳 Result
型別即可; 如果不想等的話, 看看型別 T
的 sizeof
值是否屬於 (sizeof(Result), sizeof(next_type)]
之間, 如果是的話就回傳 next_type
, 如果不是就在剩餘的型別中繼續尋找.
有了上述輔助的超函式, 我們就可以正式實作 std::make_unsigned
和 std::make_signed
:
template <typename T>
struct make_signed {
using type = copy_cv_t<T,
typename make_signed_or_unsigned_auxiliary<remove_cv_t<T>, signed char, typename signed_integral::remaining>::type
>;
};
template <typename T>
struct make_unsigned {
using type = copy_cv_t<T,
typename make_signed_or_unsigned_auxiliary<remove_cv_t<T>, unsigned char, typename unsigned_integral::remaining>::type
>;
};
我們已經給出了型別集合中的第一個型別作為 Result
引數, 那麼只需要剩餘的集合即可, 當然將整個 signed_integral
或者 unsigned_integral
直接作為引數傳入也不會得到錯誤的回傳型別, 只是多了一次判斷而已. 最後要注意, 把 const
和 volatile
標識複製回去.
有了這個思路, 接下來的型別提升系列函式實作起來非常簡單 :
template <typename T, typename = void>
struct integral_promotion_auxiliary {
static_assert(is_integral_v<T>, "This trait requires an integral type!");
};
template <typename T>
struct integral_promotion_auxiliary<T, enable_if_t<is_signed_v<T>>> {
using type = conditional_t<
is_same_v<typename promotion_index<T, signed_integral>::type::type, __int128_t>,
__int128_t, typename promotion_index<T, signed_integral>::type::remaining::type>;
};
template <typename T>
struct integral_promotion_auxiliary<T, enable_if_t<is_unsigned_v<T>>> {
using type = conditional_t<
is_same_v<typename promotion_index<T, unsigned_integral>::type::type, __uint128_t>,
__uint128_t, typename promotion_index<T, unsigned_integral>::type::remaining::type>;
};
template <typename T, typename = void>
struct floating_point_promotion_auxiliary {
static_assert(is_floating_point_v<T>, "The trait requires a floating point type!");
};
template <typename T>
struct floating_point_promotion_auxiliary<T, enable_if_t<is_floating_point_v<T>>> {
using type = conditional_t<
is_same_v<typename promotion_index<T, floating_point>::type::type, long double>,
long double, typename promotion_index<T, floating_point>::type::remaining::type>;
};
template <typename T, typename = void>
struct character_promotion_auxiliary {
static_assert(is_character_v<T>, "The trait requires a character type!");
};
template <typename T>
struct character_promotion_auxiliary<T, enable_if_t<is_character_v<T>>> {
using type = conditional_t<
is_same_v<typename promotion_index<T, character>::type::type, char32_t>,
char32_t, typename promotion_index<T, character>::type::remaining::type>;
};
template <typename T, typename = void>
struct arithmetic_promotion_auxiliary {
static_assert(is_integral_v<T> or is_floating_point_v<T>,
"The trait requires a character type or an integral type!");
};
template <typename T>
struct arithmetic_promotion_auxiliary<T, enable_if_t<is_integral_v<T>>> :
copy_cv<T, typename integral_promotion_auxiliary<remove_cv_t<T>>::type> {};
template <typename T>
struct arithmetic_promotion_auxiliary<T, enable_if_t<is_floating_point_v<T>>> :
copy_cv<T, typename floating_point_promotion_auxiliary<remove_cv_t<T>>::type> {};
template <typename T>
struct integral_promotion {
using type = copy_cv_t<T, typename integral_promotion_auxiliary<remove_cv_t<T>>::type>;
};
template <typename T>
struct floating_point_promotion {
using type = copy_cv_t<T, typename floating_point_promotion_auxiliary<remove_cv_t<T>>::type>;
};
template <typename T>
struct character_promotion {
using type = copy_cv_t<T, typename character_promotion_auxiliary<remove_cv_t<T>>::type>;
};
template <typename T>
struct arithmetic_promotion : arithmetic_promotion_auxiliary<T> {};
4. 樣板類別萃取
class_template_traits
用於萃取樣板參數中的樣板, 包括獲得其樣板引數和樣板類別.
template <typename ...>
struct class_template_traits;
template <template <typename ...> typename T, typename ...Args>
struct class_template_traits<T<Args...>> {
using type = T<Args...>;
template <typename ...Ts>
using rebind = T<Ts...>;
using arguments = type_container<Args...>;
};
5. 邏輯操作
std::conjunction
, std::disjunction
和 std::negation
這三個超函式實際上是一系列布林表達式的樣板超函式形式. 我們將 p_{1} \vee p_{2} \vee ... \vee p_{n} 稱為析取範式; 將 p_{1} \wedge p_{2} \wedge ... \wedge p_{n} 稱為合取範式; 將 \sim p 稱為否定. 用 C++ 的方法分別表示為 p_{1} \text { and } p_{2} \text { and } ... \text { and } p_{n}
, p_{1} \text { or } p_{2} \text { or } ... \text { or } p_{n}
和 \text {not } p
. 因此, 這三個超函式給定的引數必定是一系列 std::bool_constant
:
template <typename BooleanExpression>
struct negation : bool_constant<not BooleanExpression::value> {};
template <typename ...>
struct conjunction : true_type {};
template <typename BooleanExpression>
struct conjunction<BooleanExpression> : BooleanExpression {};
template <typename BooleanExpressionLHS, typename BooleanExpressionRHS>
struct conjunction<BooleanExpressionLHS, BooleanExpressionRHS> :
bool_constant<BooleanExpressionLHS::value and BooleanExpressionRHS::value> {};
template <typename BooleanExpressionLHS, typename BooleanExpressionRHS, typename ...BooleanExpressions>
struct conjunction<BooleanExpressionLHS, BooleanExpressionRHS, BooleanExpressions...> :
conjunction<conjunction<BooleanExpressionLHS, BooleanExpressionRHS>, BooleanExpressions...> {};
template <typename ...>
struct disjunction : false_type {};
template <typename BooleanExpression>
struct disjunction<BooleanExpression> : BooleanExpression {};
template <typename BooleanExpressionLHS, typename BooleanExpressionRHS>
struct disjunction<BooleanExpressionLHS, BooleanExpressionRHS> :
bool_constant<BooleanExpressionLHS::value or BooleanExpressionRHS::value> {};
template <typename BooleanExpressionLHS, typename BooleanExpressionRHS, typename ...BooleanExpressions>
struct disjunction<BooleanExpressionLHS, BooleanExpressionRHS, BooleanExpressions...> :
disjunction<disjunction<BooleanExpressionLHS, BooleanExpressionRHS>, BooleanExpressions...> {};
6. std::common_type
std::common_type
接受一系列的樣板引數, 用於確定這些樣板引數之間是否存在共用型別, 即是否存在某一個型別使得所有型別都可以向其發生隱含型別轉化. 如果存在, std::common_type
中會存在一個名稱為 type
的型別別名作為回傳型別; 否則, std::common_type
中不存在任何成員.
這個超函式的實作非常簡單, 但是需要用到一個技巧 : true ? expr1 : expr2
. 如若 expr1
對應的型別 T1
和 expr2
對應的型別 T2
可以發生某種隱含型別轉化, 那麼該表達式的結果就是發生隱含型別轉化之後對應的型別 T
; 如若他們無法發生隱含型別轉化, 則會產生編碼錯誤 :
template <typename ...>
struct common_type {};
template <typename ...Ts>
using common_type_t = typename common_type<Ts...>::type;
template <typename T>
struct common_type<T> {
using type = common_type_t<T, T>;
};
template <typename T, typename U>
struct common_type<T, U> {
using type = decltype(make_true<T, U> ? declval<decay_t<T>>() : declval<decay_t<U>>());
};
template <typename T, typename U, typename V, typename ...Ts>
struct common_type<T, U, V, Ts...> : common_type<common_type_t<T, U>, V, Ts...> {};
7. std::common_reference
這個超函式是在 C++ 20 引入 <type_traits>
的, 和 std::common_type
類似, 但是這個超函式是用於確定給定的一系列樣板引數中是否存在共用的參考型別.
首先, 我們定義一個輔助的型別別名 :
template <typename T, typename U>
using common_ref_auxiliary = decltype(make_true<T, U> ? declval<T (&)()>()() : declval<U (&)()>()());
接下來, 針對 T &
和 U &
, T &&
和 U &&
, T &
和 U&&
, T &&
和 U &
四種情況進行偏特製化 :
template <typename T, typename U, typename = void>
struct common_ref_impl {};
template <typename T, typename U>
using common_ref = typename common_ref_impl<T, U>::type;
template <typename T, typename U>
struct common_ref_impl<T &, U &, void_t<common_ref_auxiliary<
add_lvalue_reference_t<copy_cv_t<T, U>>, add_lvalue_reference_t<copy_cv_t<U, T>>>>> {
using type = common_ref_auxiliary<
add_lvalue_reference_t<copy_cv_t<T, U>>, add_lvalue_reference_t<copy_cv_t<U, T>>>;
};
template <typename T, typename U>
using common_ref_make_rvalue_ref = add_rvalue_reference_t<remove_reference_t<common_ref<T &, U &>>>;
template <typename T, typename U>
struct common_ref_impl<T &&, U &&, enable_if_t<is_convertible_v<T &&, common_ref_make_rvalue_ref<T, U>>
and is_convertible_v<U &&, common_ref_make_rvalue_ref<T, U>>>> {
using type = common_ref_make_rvalue_ref<T, U>;
};
template <typename T, typename U>
using common_ref_for_const_ref = common_ref<const T &, U &>;
template <typename T, typename U>
struct common_ref_impl<T &&, U &, enable_if_t<is_convertible_v<T &&, common_ref_for_const_ref<T, U>>>> {
using type = common_ref_for_const_ref<T, U>;
};
template <typename T, typename U>
struct common_ref_impl<T &, U &&> : common_ref_impl<U &&, T &> {};
上面同樣將 SFINAE 運用到了偏特製化中. 除此之外, 還需要針對類別樣板的情況實作輔助超函式 :
template <typename T, typename U, template <typename> typename TQual, template <typename> typename UQual>
struct basic_common_reference {};
template <typename T>
struct copy_ref_for_class_template {
template <typename U>
using type = copy_cv_t<T, U>;
};
template <typename T>
struct copy_ref_for_class_template<T &> {
template <typename U>
using type = add_lvalue_reference_t<copy_cv_t<T, U>>;
};
template <typename T>
struct copy_ref_for_class_template<T &&> {
template <typename U>
using type = add_rvalue_reference_t<copy_cv_t<T, U>>;
};
template <typename T, typename U>
using basic_common_ref = typename basic_common_reference<remove_cvref_t<T>, remove_cvref_t<U>,
copy_ref_for_class_template<T>::template type, copy_ref_for_class_template<U>::template type>::type;
然後羅列不同的情況 :
template <typename T, typename U, int Bullet = 1, typename = void>
struct common_reference_auxiliary : common_reference_auxiliary<T, U, Bullet + 1> {};
template <typename T, typename U>
struct common_reference_auxiliary<T &, U &, 1, void_t<common_ref<T &, U &>>> {
using type = common_ref<T &, U &>;
};
template <typename T, typename U>
struct common_reference_auxiliary<T &&, U &&, 1, void_t<common_ref<T &, U &>>> {
using type = common_ref<T &&, U &&>;
};
template <typename T, typename U>
struct common_reference_auxiliary<T &, U &&, 1, void_t<common_ref<T &, U &&>>> {
using type = common_ref<T &, U &&>;
};
template <typename T, typename U>
struct common_reference_auxiliary<T &&, U &, 1, void_t<common_ref<T &&, U &>>> {
using type = common_ref<T &&, U &>;
};
template <typename T, typename U>
struct common_reference_auxiliary<T, U, 2, void_t<basic_common_ref<T, U>>> {
using type = basic_common_ref<T, U>;
};
template <typename T, typename U>
struct common_reference_auxiliary<T, U, 3, void_t<common_ref_auxiliary<T, U>>> {
using type = common_ref_auxiliary<T, U>;
};
template <typename T, typename U>
struct common_reference_auxiliary<T, U, 4, void_t<typename common_type<T, U>::type>> {
using type = typename common_type<T, U>::type;
};
template <typename T, typename U>
struct common_reference_auxiliary<T, U, 5, void> {};
最後就可以獲得最終的型別輸出結果 :
template <typename ...>
struct common_reference {};
template <typename ...Ts>
using common_reference_t = typename common_reference<Ts...>::type;
template <typename T>
struct common_reference<T> {
using type = T;
};
template <typename T, typename U>
struct common_reference<T, U> : common_reference_auxiliary<T, U> {};
template <typename T, typename U, typename ...Ts>
struct common_reference<T, U, Ts...> : common_reference<common_reference_t<T, U>, Ts...> {};
8. 記憶體佈局
std::aligned_storage
, std::aligned_union
和 type_with_alignment
都是和記憶體佈局相關的型別萃取.
8.1 std::aligned_storage
std::aligned_storage
適用於獲得一個能夠存儲 POD 型別的記憶體佈局 :
#include <type_traits>
#include <iostream>
struct A {
int a;
A(int a, int b) : a {(a + b) / 2} {}
};
int main(int argc, char *argv[]) {
using A_POD = std::aligned_storage_t<sizeof(A), alignof(A)>;
A_POD a, b;
new (&a) A(10, 20);
b = a;
std::cout << reinterpret_cast<A &>(b).a << std::endl;
}
其實作方式和 std::make_unsigned
和 std::make_signed
差不多, 不過它是尋找最大的對齊 :
struct aligned_storage_double_helper {
long double _1;
};
struct aligned_storage_double4_helper {
double _1[4];
};
using aligned_storage_helper_types = type_container<unsigned char, unsigned short, unsigned int,
unsigned long, unsigned long long, double, long double, aligned_storage_double_helper,
aligned_storage_double4_helper, int *>;
template <size_t Align>
struct alignas(Align) overaligned {};
template <typename, size_t>
struct aligned_storage_find_suitable_type;
template <size_t Align, typename T, typename ...Args>
struct aligned_storage_find_suitable_type<type_container<T, Args...>, Align> {
using type = conditional_t<Align == alignof(T), T,
typename aligned_storage_find_suitable_type<type_container<Args...>, Align>::type>;
};
template <size_t Align>
struct aligned_storage_find_suitable_type<type_container<>, Align> {
using type = overaligned<Align>;
};
template <size_t Length, size_t Align1, size_t Align2>
struct aligned_storage_selector {
private:
constexpr static auto min {Align1 < Align2 ? Align1 : Align2};
constexpr static auto max {Align1 < Align2 ? Align2 : Align1};
public:
constexpr static auto value {Length < max ? min : max};
};
template <typename, size_t>
struct aligned_storage_find_max_align;
template <size_t Length, typename T>
struct aligned_storage_find_max_align<type_container<T>, Length> : alignment_of<T> {};
template <size_t Length, typename T, typename ...Args>
struct aligned_storage_find_max_align<type_container<T, Args...>, Length> : constant<size_t,
aligned_storage_selector<Length, alignof(T),
aligned_storage_find_max_align<type_container<Args...>, Length>::value>::value> {};
template <size_t Length, size_t Align =
aligned_storage_find_max_align<aligned_storage_helper_types, Length>::value>
struct aligned_storage {
private:
using aligner = typename aligned_storage_find_suitable_type<
aligned_storage_helper_types, Align>::type;
public:
union type {
aligner align;
unsigned char data[(Length + Align - 1) / Align * Align];
};
};
8.2 std::aligned_union
std::aligned_union
和 std::aligned_storage
差不多, 但是用於存儲等位的資料 :
#include <type_traits>
#include <iostream>
union U {
int i;
char c;
double d;
U(const char *str) : c {str[0]} {}
};
int main(int argc, char *argv[]) {
using U_POD = std::aligned_union_t<sizeof(U), int, char, double>;
U_POD a, b;
new (&a) U("hello world!");
b = a;
std::cout << reinterpret_cast<U &>(b).i << std::endl;
}
因此實作 std::aligned_union
和實作 std::aligned_storage
有相似之處 :
template <typename, typename ...>
struct aligned_union_find_max_align;
template <typename T>
struct aligned_union_find_max_align<T> : alignment_of<T> {};
template <typename T, typename U>
struct aligned_union_find_max_align<T, U> :
conditional<alignof(T) < alignof(U), alignment_of<U>, alignment_of<T>> {};
template <typename T, typename U, typename ...Args>
struct aligned_union_find_max_align<T, U, Args...> : conditional_t<alignof(T) < alignof(U),
aligned_union_find_max_align<U, Args...>, aligned_union_find_max_align<T, Args...>> {};
template <size_t, size_t ...>
struct aligned_union_find_max_number;
template <size_t N>
struct aligned_union_find_max_number<N> : constant<size_t, N> {};
template <size_t N, size_t M>
struct aligned_union_find_max_number<N, M> :
conditional_t<N < M, constant<size_t, M>, constant<size_t, M>> {};
template <size_t N, size_t M, size_t ...Numbers>
struct aligned_union_find_max_number<N, M, Numbers...> :
aligned_union_find_max_number<aligned_union_find_max_number<N, M>::value, Numbers...> {};
template <size_t Length, typename ...Ts>
struct aligned_union {
struct type {
alignas(aligned_union_find_max_align<Ts...>::value)
char data[aligned_union_find_max_number<Length, sizeof...(Ts)>::value];
};
};
8.3 type_with_alignment
type_with_alignment
用於獲得一個對齊方式是指定 POD 型別指定倍數的型別.
union type_with_alignment_max_alignment {
char _1;
short _2;
int _3;
long _4;
long long _5;
__int128_t _6;
float _7;
double _8;
long double _9;
};
using type_with_alignment_helper_types = type_container<
char, short, int, long, long long, double, long double>;
template <typename, size_t>
struct type_with_alignment_find_suitable_type;
template <size_t Align>
struct type_with_alignment_find_suitable_type<type_container<>, Align> {
using type = type_with_alignment_max_alignment;
};
template <size_t Align, typename T, typename ...Args>
struct type_with_alignment_find_suitable_type<type_container<T, Args...>, Align> {
using type = conditional_t<Align < alignof(T), T,
typename type_with_alignment_find_suitable_type<type_container<Args...>, Align>::type>;
};
template <size_t Align>
struct type_with_alignment : type_with_alignment_find_suitable_type<type_with_alignment_helper_types, Align> {};
9. std::has_unique_object_representations
std::has_unique_object_representations
是 C++ 17 引入 <type_traits>
的, 其接受一個樣板引數 T
. 要明白這個型別萃取怎麼使用, 我需要從源頭開始說起. 一個是型別的值表達, 另外一個是型別的物件表達. 所謂一個型別 T
的值表達是一個 unsigned char
陣列對應的佔位, 陣列大小等同於 sizoef(T)
; 所謂一個型別 T
的物件表達是持有型別 T
的位元集合. 若型別 T
的複製操作是 trivial 的, 那麼顯然值表達是物件表達中確定值的一組位元集合. 對於類別
struct s {
char c;
int i;
};
一般來說, sizeof(s)
的值為 8
. 根據記憶體對位, 編碼器要在變數 c
後面插入三個位元組, 這三個位元組是物件存儲的一部分, 也就是物件表達的一部分. 但是你更改這三個位元組的值, 通常並不會影響 s
的值 :
inline bool operator==(const s &lhs, const s &rhs) noexcept {
return lhs.c == rhs.c and lhs.i == rhs.i;
}
但是如果把 return
陳述式改為 return std::memcmp(&lhs, &rhs, sizeof lhs) == 0;
, 結果就不太一樣了. 因為 std::memcmp
是通過逐個位元進行比較的, 這其中也會包含那三個原本應該空白的位元組的部分. 然而對於類別
struct s2 {
int c;
int i;
};
來說, 就不會存在這樣的問題.
對於 std::has_unique_object_representations
, 若
T
的複製操作是 trivial 的;T
的任意兩個相同物件具有相同的物件表達,
那麼超函式 std::has_unique_object_representations
的回傳型別為 std::true_type
, 否則為 std::false_type
. 那麼我們什麼時候會使用 std::has_unique_object_representations
呢? 考慮逐個位元組甚至逐個位元進行雜湊的情況.
實作 std::has_unique_object_representations
需要用到編碼器魔法 (《【C++】實作 <type_traits>
(上)》第 6 節), 一般為 __has_unique_object_representations
.
10. std::is_pointer_interconvertible_base_of
std::is_pointer_interconvertible_base_of
是 C++ 20 引入的, 其接受兩個樣板引數 Base
和 Derived
, 用於判斷型別 Derived
是否是無歧義地從基礎類別 Base
衍生 (reinterpret_cast<Base &>(declval<Derived &>())
必須合法且有結果), 且每個 Derived
物件與其 Base
子物件的指標可以相互轉化. 其要求 :
Derived
必須是一個完整型別;Base
必須是一個完整型別, 且必須帶有const
或者volatile
標識;- 可以用
Derived
產生一個左值,
那麼即使 Derived
是私有繼承自 Base
, 那麼 std::is_pointer_interconvertible_base_of
都會回傳 true
.
從目前網路上的資料來看, 我只能寫出一個宣告 :
template <typename Base, typename Derived>
struct is_pointer_interconvertible_base_of;
11. std::is_layout_compatible
如果一個類別滿足
- 非靜態成員變數的訪問權限都相同;
- 不存在虛擬函式;
- 不是從虛擬基礎類別繼承;
- 所有非靜態成員變數和基礎類別中的所有非靜態成員變數都滿足標準佈局;
- 其基礎類別也必須是標準佈局的;
- 成員中不存在非標準佈局的成員或者參考;
- 該類別和其所有基礎類別中的非靜態成員變數和位元欄位都是首次宣告的 (例如不存在
B
繼承A
,C
繼承A
,D
繼承自B
和C
, 這樣D
中就有兩份A
, 就不再是首次宣告的);
那麼這個類別就是標準佈局的, 可以使得超函式 std::is_layout_compatible
回傳 std::true_type
; 否則, std::is_layout_compatible
回傳 std::false_type
.
std::is_layout_compatible
的實作也需要借助編碼器魔法, 一般名稱為 __is_layout_compatible
.
10. 萃取函式
std::is_pointer_interconvertible_with_class
, std::is_corresponding_member
和 std::is_constant_evaluated
都是被 constexpr
標識的函式, 而不是樣板類別. 這三個函式都是 C++ 20 引入的.
std::is_pointer_interconvertible_with_class
是確定對於 T Class::*
, 其要存取的成員變數是否屬於 Class
, 如果屬於, 那麼回傳 true
; 否則, 回傳 false
. 其函式宣告為 :
template<class S, class M>
constexpr bool is_pointer_interconvertible_with_class(M S::*mp) noexcept;
另外, 如果 S
不是標準佈局, M
不是物件的型別又或者傳入的指標為空, 那麼其永遠回傳 false
. 對於它的用法, 我舉個實例 :
#include <type_traits>
#include <iostream>
struct A {
int a;
};
struct B {
int b;
};
struct C : A, B {};
int main(int argc, char *argv[]) {
std::cout << std::is_pointer_interconvertible_with_class(&C::a) << std::endl; // 輸出結果 : true, &C::a 的型別是 int A::*, a 是類別 A 的成員變數
std::cout << std::is_pointer_interconvertible_with_class<B>(&C::a) << std::endl; // 輸出結果 : false, &C::a 的型別是 int A::*, a 是類別 B 的成員變數
}
雖然 Clang 自帶的 libc++ 標準樣板程式庫暫時 (2022 年 6 月 6 日) 沒有引入這個函式. 不過我們自己實作的話, 實作方式大致是 :
template <typename T, typename Class>
inline constexpr bool is_pointer_interconvertible_with_class(T Class::*mp) noexcept {
return __builtin_is_pointer_interconvertible_with_class(mp);
//return __libcpp_is_pointer_interconvertible_with_class(mp);
}
這也是編碼器魔法的一種, 只不過這是函式形式的罷了, 我們自己並不能通過語言層面進行實作.
std::is_corresponding_member
宣告為
template<class S1, class S2, class M1, class M2>
constexpr bool is_corresponding_member(M1 S1::*mp, M2 S2::*mq) noexcept;
要說明它的用途, 必須介紹一個名為公共起始序列的概念. 簡單地來說, 對於類別 S1
和 S2
, 將其放入等位中 :
union U {
struct S1 {
T1 a;
T2 b;
T3 c;
// ...
};
struct S2 {
U1 a;
U2 b;
U3 c;
// ...
};
};
如果存取 U.S1.a
和 存取 U.S2.a
相同, 那麼我們說 U.S1.a
和 U.S2.a
位於類別 S1
和 S2
的公共序列部分. 對於公共起始序列, 是公共序列部分的頭部. 那麼此時, std::is_corresponding_member
的用處就很清楚了 : 它用於確定 mp
與 mq
是否指代 S1
與 S2
的公共起始序列中的對應成員.
std::is_constant_evaluated
它用於檢查一個表達式是否可以在編碼期進行計算, 可以實作為
constexpr bool is_constant_evaluated() noexcept {
return __builtin_is_constant_evaluated();
}
std::is_constant_evaluated
在 C++ 20 中加入, 在 C++ 2b (tentatively C++23) 中被遺棄 (可參考《【C++】constexpr
, consteval
和 constinit
》).
自創文章, 原著 : Jonny. 如若閣下需要轉發, 在已經授權的情況下請註明本文出處 :