boost::signals2 が便利そうな件について

結構 音速が遅い情報かもですが、すごく便利なライブラリが boost に追加されていたので久々に随筆更新。
最近、ぼちぼちゲーム開発(正確にはゲーム開発用のライブラリ開発)してたりするのですが、ゲームって「毎フレーム何かをさせる」って処理が多いです。
たとえばシューティングなら、毎フレーム入力を見に行って自機を動かさなきゃいけないし、敵の行動も制御しなきゃいけないし、弾も動かさなきゃいけないし、エトセトラエトセトラ。
こういう処理をさせる場合、各処理を細かく関数化(ないしクラス化)して、関数のリストを作り、そのリストに登録された関数を毎フレーム実行、というのが一つの流れになるわけですが、実はこれ、実装するのがいろいろ面倒。
そういう処理用に boost::signals ( signals2 の前バージョン)が存在することも知っていたのですが、これ、リンクが必要だったり同グループの各スロットの呼び出し順が不定だったりと使いにくかったので、こりゃダメだとがっかり。
んで、 signals がそんなもんだから、 signals2 もどうせ似たようなものだろう、と勝手に思い込んで、 signals2 が発表された頃には全然見向きもしなかったのですが、最近になって調べてみると、

  • 実行順序が安定。新たに登録する関数(スロット)を最初に実行するように指定も出来るし、最後に実行(デフォルト)させることも出来る
  • mutex によるマルチスレッド対応
  • connect_extended 。簡単に言うと呼び出したスロット内で「スロット実行を一時中断する」「もうそのスロットを呼び出さない」ように出来る
  • なにより、ライブラリのリンクが要らない!

など、ゲームライブラリとして使うには十二分な機能が備わっているようで。
実際にチュートリアルhttp://boost-sandbox.sourceforge.net/doc/html/signals2/tutorial.html 和訳: http://docs.google.com/Doc?id=ddcwmgjq_12hdhr8tcw )を見てみると、構文も( boost 使いにとっては)素直で扱いやすくていい感じ。なので、今作ってるゲームライブラリは、こいつをベースに作るのがよさげです。



とまぁそんな boost::signals2 ですが、実は個人的に気に食わない部分も二つばかりあったりします。


まず「スロット実行中に他のスロットを connect した場合、すぐに登録されたスロットが実行されるか、それとも次のシグナル呼び出しのときに実行されるかが不定である」( It is unspecified whether connecting a slot while the signal is calling will result in the slot being called immediately. 英語苦手なので間違って解釈してるかもしれないが^^; )らしいこと。
というわけで、実際にテストプログラム書いてみました:

// スロット呼び出し中のスロット追加するテスト
#include <boost/signals2/signal.hpp>
#include <iostream>
#include <string>

using namespace std;
namespace bs2 = boost::signals2; // 簡単のため

// n 回メッセージ出力する拡張スロット
struct message_viewer
{
  explicit message_viewer( const string& msg_, int n_ = 1 )
    : msg( msg_ ), n( n_ ) {}
  
  typedef void result_type;
  result_type operator()( const bs2::connection& conn )
  {
    if( n > 0 ){ --n; } // n が負の場合は無限回表示
    if( n == 0 ){ conn.disconnect(); }
    
    cout << msg;
  }
  
 private:
  string msg;
  int n;
  
}; 

typedef bs2::signal< void () > sig_t;

#define PRINT_AND_EXECUTE( expr ) \
  cout << #expr << ";\n"; expr;

// 自身を呼び出したシグナルにスロットを接続する拡張スロット
struct hoge
{
  hoge( sig_t& sig_ ) : sig(sig_), frame(0) {}
  
  sig_t& sig;
  int frame;
  
  void operator()( const bs2::connection& conn )
  {
    if( frame == 0 )
    {
      // connect_extended で「 connection を引数に取る関数」をシグナルに接続できる
      PRINT_AND_EXECUTE( sig.connect_extended( message_viewer( "foo\n", 3 ) ) );
    }
    else if( frame == 1 )
    {
      PRINT_AND_EXECUTE( sig.connect_extended( message_viewer( "bar\n", 2 ) ) );
    }
    else
    {
      // おわり
      PRINT_AND_EXECUTE( conn.disconnect() );
      
      return;
    }
    
    ++frame;
  }
  
};

int main()
{
  sig_t sig;
  
  // テストスロットを登録
  sig.connect_extended( hoge( sig ) );
  
  // 実行すべきスロットがなくなるまで実行
  int framecount = 0;
  while( !sig.empty() )
  {
    cout << "entering frame " << ++framecount << "...\n";
    sig();
  }
};

こいつを cygwin の g++ で実行してみると、

entering frame 1...
sig.connect_extended( message_viewer( "foo\n", 3 ) );
entering frame 2...
sig.connect_extended( message_viewer( "bar\n", 2 ) );
foo
entering frame 3...
conn.disconnect();
foo
bar
entering frame 4...
foo
bar

って感じに出力されました。どうやら g++ では次のシグナル呼び出しで実行されるようですが、単純にテストが簡単すぎたのかもしれないのでよく分かりません(二回しか接続してないですしね)。
まーとにかく、ドキュメント読む限り不定なそうなので、スロット実行中に他のスロットを登録したい場合(ゲームプログラムではよくあること)には、改良が必要だと思われます。
改良するとしたら「スロット呼び出し中の connect 呼び出しはキューにでも退避しておいて、呼び出しが終わったら(あるいは次の呼び出しの直前に)まとめて接続」てな仕掛けにすればいいので、割と簡単ですね。


もう一つの不満点は、今回もスロット実行中に他のスロットを登録する場合なのですが、「スロットの呼び出し元のシグナルを簡単に調べる方法がない」点。さっきのテストプログラムではオブジェクトのメンバに参照を持たせておき、構築時に引数として渡していましたが、出来ればそんな手間は避けたいところです。
一番よいのは connection クラスから接続先の signal を得られる事ですね。とはいえ connection は全ての signal テンプレート共通で使うクラスなので、中々難しいのかもしれません。


というわけで、今製作中のゲーム開発用ライブラリで、その辺の不満点を改善しようかと思っていたりいなかったり。ちょっと頑張るとするかー。