後記:以下主要是展示template處理explicit bool operator的檢查所用的技巧,但其實真正要做cast檢查這樣檢查太瑣碎也不全面,可以透過std::is_constructible和std::is_convertible來處理,留待之後的文章整理
在C++中如果我們要知道物件轉成bool的值,可以直接用cast operator將型別轉成bool
例如 (bool)10.0 => true
但是像是
class X { }; X x; (bool)x;
這段代碼顯然是無法通過編譯的(invalid cast from type ‘X’ to type ‘bool’),
因為X沒有定義bool轉型的函式
假如我們想要設計一個function GetBool 可以檢查物件是否可轉成bool
─ 如果可以轉型就返回轉型的bool value, 如果不能轉型就返回false
因為使用的型別事先未知,所以勢必要使用template,根據使用者呼叫的參數來決定回傳值。因此很直覺的可以想到應該可以這樣做:
template<typename T> bool GetBool(T t) { return (bool)t; }
但是這樣做有個問題 ─ 那就是前面提到的,有些類別型別不支援bool轉型(沒有實作bool operator) ,必須在compile time要分兩個function,一個是可以轉型的,一個是不能轉型的,可是要讓呼叫者自己分辨傳入的參數顯然不太方便,這時候可以透過template metaprogramming的一些技巧讓compiler在compile time自動推導處理
這邊展示的GetBool作法參考了 gcc libstdc++-v3的is_convertible的概念實作
─ 透過helper class推導”type” member field的型別
std::true_type /std::false_type
─ 再利用繼承該std::integral_constant型別讓後續template處理取得取得對應的bool value
#include <type_traits> #include <iostream> #include <functional> #include <memory> template <typename T> class has_bool_operator_helper { private: template <typename C, typename = decltype(&C::operator bool)> static std::true_type test(int) ; template <typename> static std::false_type test(...); public: typedef decltype(test<T>(0)) type; }; template <typename T> class has_bool_operator: public has_bool_operator_helper<T>::type { };
透過decltype(test<T>(0))在compile決定"type" member field的型別
test<T>(0)會讓compiler選擇我們定義的兩個function
至於哪一個function會被implicit convertion和overload resolution來決定
例如test<T>(0) -> 他會優先選擇test(int)
因為0的型別本身就是int,事實上如果比對的函式如果是test(bool)也會優先於test(...)比對,原因是standard conversion sequencec會優先於ellopsis conversion sequence
has_bool_operator_helper這邊使用SFINAE的技巧, 可參考https://en.cppreference.com/w/cpp/language/sfinae
overload resolution過程選擇後如果發生subsitution fail,compiler不會報錯,而是繼續找下一個符合的function。
因此如果decltype(&C::operator bool)>失敗就會採用static std::false_type test(…)的function
但是operator bool是用於class,如果對於一些fundamental types或是pointer等就不適用。
因此需要利用std type_traits提供的is_convertible檢查是否可轉型,注意is_convertible他是透過implicit conversion的方式利用類似上面提到的方法來測試 因此不能implicit conversion到bool的型別is_convertible會回報false
GetBool就利用std::is_convertible和has_bool_operator來判斷型別是否支援bool轉型 在下面分成3類
─ is_convertible
─ !is_convertible && has_bool_operator
─ !is_convertible && !has_bool_operator
分成這3類主要是為了更清晰的展示template判斷的規則
事實上其實只要分成
─ is_convertible || has_bool_operator
─ !is_convertible && !has_bool_operator
這兩類就可以
template <class T> typename std::enable_if<std::is_convertible<T, bool>::value, bool>::type GetBool(T t) { std::cout << "choose convertible" << std::endl; return (bool)t; } template <class T> typename std::enable_if<!std::is_convertible<T, bool>::value && !has_bool_operator<T>::value, bool>::type GetBool(T t) { std::cout << "choose false" << std::endl; return false; } template <class T> typename std::enable_if<!std::is_convertible<T, bool>::value && has_bool_operator<T>::value, bool>::type GetBool(T t) { std::cout << "choose bool operator" << std::endl; return (bool)t; }
template<typename T> void test(T x) { std::cout << GetBool(x) << std::endl; } class X1 { }; class X2 { public: operator bool() const { return true; } }; class X3 { public: explicit operator bool() const { return true; } }; class X4 { //private operator bool() const { return true; } }; X1 x1; X2 x2; X3 x3; X4 x4; void f1(){} std::function<void()> f2; std::function<void()> f3 = f1; std::shared_ptr<int> sp1; std::shared_ptr<int> sp2(new int); enum en{E1, E2}; en e1 = E1; en e2 = E2; int* p1 = NULL; int p2 = 0; int* p3 = &p2; int arr[5]; std::string s1;
int main() { test(f1); //convertible => 1 test(f2); //bool operator => 0, std::function的bool operator, f2沒有assign, 返回false test(f3); //bool operator => 1, std::function的bool operator test(x1); //choose false, X1沒有定義bool conversion test(x2); //convertible => 1, X2定義bool conversion (因為可以implicit conversion 所以走is_convertible那條路) test(x3); //bool operator => 1, X3定義explicit bool conversion(因為無法implicit conversion 所以走bool operator check那條路) test(x4); //choose false, X4雖然定義bool conversion但是private test(true); //convertible=>1 test(false); //convertible=>0 test(100.0); //convertible=>1 test(0.0); //convertible=>0 test(-1); //convertible=>1 test(p1); //convertible=>0, null pointer test(p2); //convertible=>0 test(p3); //convertible=>1, not null pointer test(nullptr); //choose false, nullptr無法implicit轉型, 也無bool operator test(sp1); //bool operator => 0, pointer not set test(sp2); //bool operator => 1 test(e1); //convertible=>0, enum value 0 test(e2); //convertible=>1, enum value 1 test(arr); //convertible=>1, array decay to pointer test(s1); //choose false, std::string無法implicit轉型, 也無bool operator return 0; }
上面的nullptr無法implicit轉型bool所以就被歸類在choose false的區域,也剛好nullptr本身的semantics也是bool false
如果因為某些型別需要例外(假如字串要空的才算false)那就可以利用template specialization。 例如:
template <> bool GetBool<const char*>(const char* s) { std::cout << "choose char" << std::endl; return s[0] != '\0'; }