torutkのブログ

ソフトウェア・エンジニアのブログ

契約による設計(Design by Contract)の実装〜事後条件判定をBoost.Lambdaの遅延評価で実現

d:id:torutk:20081013で検討したC++事後条件コードを、クロージャの概念が利用できるBoost.Lambdaライブラリを使って実装します。

実装イメージ

事後条件のイメージとしては、以下のコードのようにメソッドの最初(でなくてもよいが)にアサート文を記述しておき、メソッドが離脱するときにアサート文を実行するものです。

// イメージなのでコンパイルは通らない
class PersonList { ... };

void PersonList::add(const Person& aPerson) {
    ENSURE(list_.size() = old list_.size + 1);
    ENSURE(list_.back() = aPerson);

    list_.push_back(aPerson); // 本来の処理
}

Boost.Lambdaのクロージャで事後条件ロジックを定義

RAIIイディオムでは、評価するロジックで必要とする変数をあらかじめRAIIオブジェクトに渡しておかなくてはなりません。通常コンストラクタの引数で渡します。すると、事後条件用RAIIオブジェクトが汎用性を失い、メンバ関数ごとにクラス定義する必要があります。

クロージャでは、その問題点を解決するために、評価するロジックそのものを関数オブジェクトとしてRAIIオブジェクトに渡します。さらに、関数オブジェクトのロジック中で使用する変数の値は、関数オブジェクト生成時ではなく、メンバ関数終了時の値でなくてはなりません。そこで、変数を束縛して遅延評価するクロージャの特徴を活用します。

RAIIオブジェクトの定義

#include <boost/function.hpp>
class PostCondition {
public:
  PostCondition(boost::function<bool()> f) : function_(f) {}
  ~PostCondition() {
    if (! function()) {
        throw PostConditionException();
    }
  }
private:
  boost::function<bool ()> function_;
};

RAIIオブジェクトPostConditionはコンストラクタで、引数無しの戻り型boolの関数オブジェクトを1つ取ります。スコープを離れるときにデストラクタでこの関数オブジェクトを実行し、結果がfalseの場合は事後条件違反を通知します。

2009/01/16追記

上記ではRAIIのデストラクタで例外をスローしていますが、これはよくない実装です。関数内で例外が発生し中断してもRAIIのデストラクタが起動し、ここで例外をスローすると、C++ではとんでもない状況になります。
ここは、メッセージを出力してabortする実装にすべきでした。

RAIIオブジェクトを使用した事後条件の実装

#include <boost/lambda/lambda.hpp>
#include <boost/lambda/bind.hpp>
   :
void PersonList::add(const Person& aPerson) {
    using namespace boost::lambda;
    PostCondition pcond1(
        bind(&std::list<Person>::size, constant_ref(list_)) == list_.size() + 1
    );
    PostCondition pcond2(
        bind( static_cast<const Person& (std::list<Person>::*)() const>
             (&std::list<Person>::back), constant_ref(list_)) == aPerson
    );
    list_.push_back(aPerson); // 本来の処理
}

1つ目の事後条件は、==の左辺が、メンバ関数addを抜ける時点でのlist_オブジェクトのsize()メンバ関数を呼び出し評価値を得るためのbindを記述しており、右辺が記述行を実行した時点でのlist_size()の評価値に1を加えた値を記述しています。

2つ目の事後条件は、==の左辺が、メンバ関数addを抜ける時点でのlist_オブジェクトのback()メンバ関数を呼び出し評価値を得るためのbindを記述しています。最初の例と違う点は、static_castでback()のシグニチャ(戻り値型と引数型)を指定していることです。

boost::lambda::bindは、メンバ関数オーバーロード定義されていると、コンパイラがどの関数定義にマッチするか決定できないのでエラーとなってしまいます。そのときは、static_castで引数型(と戻り値型)を指定することでコンパイラにどの関数定義とマッチするか決定できるようにします。ちょっとBoost.Lambdaのいけてない点です。C++0xで正式にクロージャが入るまでの辛抱です。

boost::lambda::constant_refの意味

1つ目の事後条件で、==の左辺のconstant_refを指定しない場合はどうなるでしょうか。以下のコードのように書いた場合です。

    PostCondition pcond1(
        bind(&std::list<Person>::size, list_) == list_.size() + 1
    );

RAIIイディオムにより関数が実行されるのはスコープを抜けるときですが、constant_refを使用しないlist_はこの事前条件RAII定義時点の状態で評価され(塩漬けされ)てしまうので、後でsize()を取っても1増えた状態にはなっていません。

一方、右辺のlist_は、addメンバ関数で要素が追加される前の値である必要があります。なので、constant_refを使用せず、bindでsize()呼び出しを遅延評価もせず、この時点での値を評価しています。

boost::lambda::bindの意味

bindは、関数アドレスと引数を結びつけるものですが、ラムダ式でもあり、今回は事後条件の式を合成する一部として使用しています。クラスのメンバ関数の場合、通常の呼び出し式は、

インスタンス変数.メンバ関数()

の形式ですが、bindの場合は、

bind(&クラス名::メンバ関数名, インスタンス変数, 引数...)

の形式となります。

ちなみに、boost::lambda::bindの場合、メンバ関数がconst関数でないと呼び出しできないようです(コンパイルエラーとなる)。

また、2つ目の事後条件でのbind使用例では、static_castを使用しています。これは、bindで束縛するメンバ関数 std::listのback()がオーバーロード定義されているからです。

// bits/stl_list.h
      reference back()
      { 
	iterator __tmp = end();
	--__tmp;
	return *__tmp;
      }

      const_reference back() const
      { 
	const_iterator __tmp = end();
	--__tmp;
	return *__tmp;
      }

&クラス名::メンバ関数名の形では、メンバ関数オーバーロードされている場合、どの関数定義のアドレスとすればよいか決定できません。そこで、関数アドレスをキャストで引数・戻り値型を指定することで、コンパイラが決定できるようにします。メンバ関数アドレスのキャストは以下のような記述例となります(なるようです)。

static_cast<const Person& (std::list<Person>::*)() const> (&std::list<Person>::back)

std::listのbackの戻り値型のreferenceやconst_referenceは、おそらくテンプレート型で指定した要素の型になると想像し、const_referenceの場合は上述のようにconst Person&としてみたらうまくコンパイラが処理できたので、このように記述しています。


このメンバ関数のbindは、なかなかうまくいかず、この半月ほど悩んでまして、今日やっと解が見えてきたところです。コンパイル・エラーを見ても、ちっとも原因が分からないので、とても苦労しました。

ちなみに、static_castの後のconstが抜けていると、どわっとコンパイルエラーになります。

../src/Person.cpp:102: error: invalid static_cast from type ‘<unresolved 
overloaded function type>’ to type ‘const Person& (std::list<Person, 
std::allocator<Person> >::*)()’
/usr/include/boost/lambda/detail/operator_lambda_func_base.hpp: In member
function ‘RET 
boost::lambda::lambda_functor_base<boost::lambda::relational_action<boost
::lambda::equal_action>, Args>::call(A&, B&, C&, Env&) const [with RET = 
bool, A = const boost::tuples::null_type, B = const boost::tuples::null_type,
 C = const boost::tuples::null_type, Env = const boost::tuples::null_type, 
Args = boost::tuples::tuple<boost::lambda::lambda_functor<boost::lambda::
lambda_functor_base<boost::lambda::action<2,boost::lambda::function_action<2, 
boost::lambda::detail::unspecified> >, 
boost::tuples::tuple<size_t (std::list<Person, std::allocator<Person> >::* const)
()const, const boost::lambda::lambda_functor<boost::lambda::identity<const 
std::list<Person, std::allocator<Person> >&> >, boost::tuples::null_type, 
boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, 
boost::tuples::null_type, boost::tuples::null_type, boost::tuples::null_type, 
boost::tuples::null_type> > >, 
  :以下省略

クロージャによる事後条件の定義(マクロによる整形)

RAIIイディオムとBoost.Lambdaによるクロージャで、事後条件の実装の1つができあがりました。あとは仕上げとして、事後条件違反時に、違反した式をメッセージとして出力するようにします。これは古臭いマクロの力で実現することにします。

#ifdef DBC
#define DBC_ENSURE(condition) \
    PostCondition postCondition(condition, __FILE__, __LINE__, #condition);
#else
#  define DBC_ENSURE(condition)
#endif
  • PostConditionクラスのコンストラクタ引数追加版は省略
  • 同じメンバ関数内に複数のDBC_ENSURE文を書く場合の対応は省略
    • DBC_ENSURE_1, DBU_ENSURE_2, ・・・のように安直にマクロを増やす?
  • エラーリターン、例外時にも事後条件判定が実行されてしまう問題に未対応
C++0xではクロージャが導入される(追記)

d:id:faith_and_brave:20081211に、C++0xC++言語仕様次期改訂版)で導入予定のクロージャについて分かりやすく説明されています。Boostより簡潔にクロージャが書けるので、いい感じに思います。