読者です 読者をやめる 読者になる 読者になる

Boost.statechart / Boost.msm を実際に使うとハマるかもしれないポイントなど。

C++

 ライブラリをアピールするとき、メリットを強調するもんです。しかしよく使われるライブラリは、その欠点や使いにくさが語られます。そうしたネガティブ面をも周知されることこそが、"使われている" という実績なのだと思います。
 なので、Boost.勉強会で紹介した二つのライブラリについてもちょっとだけネガティブなポイントを書いておこうと思います。

■Boost.statechart

1.定義順がおかしいとコンパイルが通らない。
 状態マシン型の定義に初期状態の指定が必要です。

namespace sc = boost::statechart;

// 状態マシンの定義
struct my_machine                             // ↓コレ
    : sc::state_machine< my_machine, my_state1 >
{};

 一方で、状態の定義に状態マシンの指定が必要です。

// 状態の定義
struct my_state1                         // ↓コレ
    : sc::simple_state< my_state1, my_machine >
{};
struct my_state2                         // ↓コレ
    : sc::simple_state< my_state2, my_machine >
{};

なので、何も考えずにこれだけ書いてコンパイルすると通りません。状態マシンを先に書くと初期状態の型が未知なのでエラー、状態を先に書くと状態マシンの型が未知なのでエラーになります。
 で、どうするかっていうと前方宣言を使います。

// 前方宣言
struct my_state1;
struct my_state2;

// 状態マシンの定義
struct my_machine
    : sc::state_machine< my_machine, my_state1 >
{};

// 状態の定義
struct my_state1
    : sc::simple_state< my_state1, my_machine >
{};
struct my_state2
    : sc::simple_state< my_state2, my_machine >
{}

 コレで解決!


2.コンテキストの取得に失敗する。
 それぞれの状態は、所属する状態マシンオブジェクトにアクセスできます。

// 状態の定義
struct my_state1
    : sc::simple_state< my_state1, my_machine >
{
    void foo()
    {
        my_machine & m = context< my_machine >();
        m.bar();
    }
};

 しかし、これをいつでもアテにして良いわけではありません。例えば
「my_state1 になったときに my_machine の bar() を呼ぶ。」
ということをしたいとき。Boost.statechart では "my_state1 になったとき" に my_state1 のコンストラクタが呼ばれるようになっています。
 しかし、こう書くと実行時エラーになります。

struct my_state1
    : sc::simple_state< my_state1, my_macine >
{
    my_state1()
    {
        my_machine & m = context< my_machine >();   // ここで実行時エラー
        m.bar();
    }
};

 コンストラクタ呼び出しの時点では、まだ状態マシンが紐付けられていないというわけです。状態オブジェクトのコンストラクタでは、context<>() の呼び出しは厳禁です。一方、Boost.msm では状態の開始を on_entry メンバ関数で捕まえるのでこの問題は発生しません。

■Boost.msm

1.不要な状態オブジェクトが生成される。
 状態 my_state1, my_state2 を行ったり来たりするだけの状態マシン my_machine を考えます。

namespace msm = boost::msm;

// 状態の定義
struct my_state1 : msm::front::state<>
{
    my_state1() { cout << "my_state1" << endl; }
};
struct my_state2 : msm::front::state<>
{
    my_state2() { cout << "my_state2" << endl; }
};

// 状態マシンの定義
struct my_machine : msm::front::state_machine_def< my_machine >
{
    struct transition_table : boost::mpl::vector<
    // | current  | event   | next     | act | grd  |
    Row< my_state1, my_event, my_state2, none, none >,
    Row< my_state2, my_event, my_state2, none, none >,
    > {};

    typedef my_state1 initial_state; // 初期状態は my_state1
};

 この状態マシンオブジェクトを生成して一度もイベントを与えなかった場合、すなわち一度も my_state2 に遷移しなかった場合、my_state2 はコンストラクトされますBoost.msm では状態マシンが生成されるときに全ての状態オブジェクトが生成されるのです。Boost.statechart では、その状態に遷移しないとそもそも生成されないのですがそれとは対照的ですね。従って大きいリソースを食う状態オブジェクトや、生成に時間がかかるような状態オブジェクトがある状態マシンは、マシンそのものの生成にコストがかかることになります。


2.mpl の限界。
 Boost.msm は transition_table にこそ価値があります。しかし transition_table は mpl::vector を使って記述するので、mpl::vector の要素数の限界がすなわち transition_table の限界です。デフォルトサイズで足りない場合はマクロを使って上限値を変更してやる必要があります。当然ながら代償にコンパイル時間を要します。
 Boost.statechart でも mpl::list を使うので同じ問題が発生しそうですが、Boost.statechart では 1 状態あたり 1 個の mpl::list を利用するのに対して、Boost.msm では 1 状態マシンあたり 1 個の mpl::vector を利用するので、Boost.msm の方が上限に達しやすいというわけです。


■Boost.statechart と Boost.msm 両方

1.動的に構造が決定する状態マシンを作れない
 まぁ、これはそのままの意味です。いずれのライブラリも、その状態マシンが取り得る状態の型はコンパイル時に明確である必要があります。


2.状態型は default-constractible でなくてはならない。
 状態は、状態マシンによって生成されます。しかしコンストラクタに渡す引数を指定できません。状態遷移はイベントによってトリガされるわけなので、イベントオブジェクトを引数に取るコンストラクタが利用できると便利なんじゃないかと思うことがありますが、少なくとも現状では不可です。全ての状態は default-constractible であることが求められます。


3.コンパイル時間がかかる。
 コンパイル時間は短ければ短いほど良いですよね。状態マシンはプログラムを構成するいち要素でしか無いので、状態マシン以外の部分の開発をしている時にコンパイル時間を奪われるのは嬉しくありません。
 例えば GUI の表示を微調整するときとか、短時間にコンパイル&実行を繰り返すときに状態マシンをいちいちコンパイルして欲しくないわけです。一方で状態マシンを使った開発では、発生するイベントの種類はさほど頻繁に変動しないと思われます。これらを考慮すると、状態マシンは pimpl にすると良いと思います。
 pimpl にするサンプルソースを書いておきます。

my_machine.h

struct my_machine
{
    my_machine();
    ~my_machine();
    
    // 転送関数。
    void process_event( my_event1 const & e1 );
    void process_event( my_event2 const & e2 );
    
    struct impl;
    impl * impl_;
};

my_machine.cpp

#include "my_machine.h"

struct my_machine::impl : state_machine< my_machine::impl, initial_state >
{
    // 状態マシンの実装は省略。
};

my_machine::my_machine()
    : impl_( new impl )
{};
my_machine::~my_machine()
{
    delete impl_;
}

// 転送関数。
void my_machine::process_event( my_event1 const & e1 )
{
    impl_->process_event( e1 );
}
void my_machine::process_event( my_event2 const & e2 )
{
    impl_->process_event( e2 );
}

 イベントの転送処理のぶんだけコード量が増えてしまうのがちとアレですが、その代償に得られるコンパイル時間の節約は多くのケースでお釣りが来ると思います。