bool conversion test

後記:以下主要是展示template處理explicit bool operator的檢查所用的技巧,但其實真正要做cast檢查這樣檢查太瑣碎也不全面,可以透過std::is_constructiblestd::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
參考 C++03 13.3.3.2 Ranking implicit conversion sequences

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';
}
This entry was posted in C++ Language. Bookmark the permalink.

Leave a Reply