More C++ Idioms/安全な bool(Safe bool)
安全な bool(Safe bool)
[編集]意図
[編集]あるクラスに、望まない式評価に含まれないよう制限した上で真偽値としてのテストを提供する。
別名
[編集]conversion to unspecified bool idiom
動機
[編集]ユーザー定義の真偽値への変換関数は利益よりも害をもたらしうる。なぜなら、それにより理想的には含まれて欲しくない式評価に、そのクラスが含まれることを許すからである。単純な変換演算子が定義されている場合、2つあるいはそれ以上の無関係なオブジェクトが比較可能になってしまい、型安全性が損なわれる。 例えば
struct Testable
{
operator bool() const {
return false;
}
};
struct AnotherTestable
{
operator bool() const {
return true;
}
};
int main (void)
{
Testable a;
AnotherTestable b;
if (a == b) { /* blah blah blah*/ }
if (a < 0) { /* blah blah blah*/ }
// 上の比較は偶発的なものであり意図したものではないが、コンパイラは喜んでコンパイルする。
return 0;
}
解法とサンプルコード
[編集]安全な bool(Safe bool)イディオムは、直感的な if 文を使ったテストという構文的な利便性を可能とするが、同時に、知らぬ間にコンパイルされてしまう意図しない文を防ぐ。 以下が、安全な bool(Safe bool)イディオムのコードである。
class Testable {
bool ok_;
typedef void (Testable::*bool_type)() const;
void this_type_does_not_support_comparisons() const {}
public:
explicit Testable(bool b=true):ok_(b) {}
operator bool_type() const {
return ok_==true ?
&Testable::this_type_does_not_support_comparisons : 0;
}
};
class AnotherTestable ... // Testable と同一。
{};
int main (void)
{
Testable t1;
AnotherTestable t2;
if (t1) {} // 期待通りに動く
if (t2 == t1) {} // コンパイルに失敗する
if (t1 < 0) {} // コンパイルに失敗する
return 0;
}
再利用可能な解法
2つのもっともらしい解法がある。実際のロジックに対する仮想関数を持つ基本クラスを使う方法と、派生関数でどの関数を呼び出せばよいかを知っている基本クラスを用いる方法である。仮想関数には(特に、真偽値テストを追加しようとしているクラスが他に仮想関数を持たない場合)コストが存在する。以下の2つのバージョンを参照せよ。
class safe_bool_base {
public:
typedef void (safe_bool_base::*bool_type)() const;
void this_type_does_not_support_comparisons() const {}
protected:
safe_bool_base() {}
safe_bool_base(const safe_bool_base&) {}
safe_bool_base& operator=(const safe_bool_base&) {return *this;}
~safe_bool_base() {}
};
// 仮想関数なしでのテスト性のため
template <typename T=void>
class safe_bool : private safe_bool_base {
// main 中でアクセス制御違反を引き起こすために、
// private 継承か、protected 継承であることが非常に重要
public:
operator bool_type() const {
return (static_cast<const T*>(this))->boolean_test()
? &safe_bool_base::this_type_does_not_support_comparisons : 0;
}
protected:
~safe_bool() {}
};
// 仮想関数ありでのテスト性のため
template<>
class safe_bool<void> : private safe_bool_base {
// main 中でアクセス制御違反を引き起こすために、
// private 継承か、protected 継承であることが非常に重要
public:
operator bool_type() const {
return boolean_test()
? &safe_bool_base::this_type_does_not_support_comparisons : 0;
safe_bool_base::this_type_does_not_support_comparisons();
}
protected:
virtual bool boolean_test() const=0;
virtual ~safe_bool() {}
};
template <typename T>
bool operator==(const safe_bool<T>& lhs, bool b) {
if (b)
{
if (lhs) return true;
else return false;
}
else
{
if (lhs) return false;
else return true;
}
}
template <typename T>
bool operator==(bool b, const safe_bool<T>& rhs) {
if (b)
{
if (rhs) return true;
else return false;
}
else
{
if (rhs) return false;
else return true;
}
}
template <typename T, typename U>
void operator==(const safe_bool<T>& lhs,const safe_bool<U>& rhs) {
lhs.this_type_does_not_support_comparisons();
}
template <typename T,typename U>
void operator!=(const safe_bool<T>& lhs,const safe_bool<U>& rhs) {
lhs.this_type_does_not_support_comparisons();
}
どのように safe_bool を用いるかを以下に示す。
#include <iostream>
class Testable_with_virtual : public safe_bool<> {
public:
virtual ~Testable_with_virtual () {}
protected:
virtual bool boolean_test() const {
// 真偽値に変化するロジックをここで実行
return true;
}
};
class Testable_without_virtual :
public safe_bool <Testable_without_virtual> // CRTP イディオム
{
public:
/* 「非」仮想 */ bool boolean_test() const {
// 真偽値に変化するロジックをここで実行
return false;
}
};
int main (void)
{
Testable_with_virtual t1, t2;
Testable_without_virtual p1, p2;
if (t1) {}
if (p1 == false)
{
std::cout << "p1 == false\n";
}
if (p1 == p2) {} // 期待通り、コンパイルできない
if (t1 != t2) {} // 期待通り、コンパイルできない
return 0;
}
C++ では、派生クラスでは protected なメンバ関数のアドレスを取ることはできない。 派生クラスは通常のクラス、クラステンプレート、クラステンプレートの特殊化になりうる。 安全な bool(Safe bool)イディオムの実装には safe_bool_base::this_type_does_not_support_comparisons を派生クラスでそのアドレスを取ることができないように protected として宣言するものがある。 再利用可能な安全な bool(Safe bool)イディオムの要求である。 不幸にも、g++ コンパイラには、protected として宣言されているとしてもクラステンプレートで safe_bool_base::this_type_does_not_support_comparisons のアドレスが取れてしまうというアクセス制御のバグが存在する。
既知の利用
[編集]- boost::scoped_ptr
- boost::shared_ptr
- boost::optional