第十六報:イベントハンドリング・タスク処理とデリゲート
今回はデリゲートについて話します。先ず Java, C# などの言語におけるイベントハンドリングに触れ、その C++ における実装法について話します。この際に出てくる重要な概念が、C# における「デリゲート」です。そのデリゲートを C++ で実装する方法と、そのイベントハンドリングやタスク処理への応用を考えたいと思います。
MPI は結局使う必要がなかったので、前回の続きは無期延期ということでお願いします。
ここで言うイベントというのは、例えば「マウスがクリックされた」「ウィンドウのサイズが変更された」「タイマーに設定した時間がきた」など、アプリケーションに起こった何らかの変化を指す言葉です。このようなイベントが発生した場合に実行される処理のことをイベントハンドラ(イベントリスナ)と言い、処理する事をイベントハンドリングといいます。
Java において、イベントハンドラはイベントハンドラ用のクラス/インタフェイスを派生/実装したクラス内で定義します。例えば、あるキーが押された場合のイベントハンドラは
import java.lang.*; import java.applet.Applet; import java.awt.*; import java.awt.event.*; public final class TestApplet extends Applet { ... 略 ... public void init() { addKeyListener(new Control); } final class Control extends KeyAdapter { public void keyPressed(KeyEvent e) { switch(e.getKeyCode()) { case KeyEvent.VK_DOWN : moveDown (); break; case KeyEvent.VK_LEFT : moveLeft (); break; case KeyEvent.VK_RIGHT: moveRight(); break; } } } } |
といった感じで実装します。KeyAdapter というクラスがイベントハンドラ用の基底クラスで、そのメソッド keyPressed がキーが押された際のイベントハンドラです。あとはこの派生クラス Control のオブジェクトを addKeyListener で登録すれば、キーが押された際に Control.keyPressed が実行されるという流れになります。複数のイベントハンドラを同時に登録し、連続実行することもできます。
これと同じ機構を C++ で実装しようとすると1つ問題が発生します。それは、Java と C++ のローカルクラスにおける仕様の違いが原因で起こります。Java ではローカルクラスはそれを含む親クラスの一部であり、親クラスのメンバに直接アクセスすることができます。しかし、C++ におけるローカルクラスは親クラスとは独立したクラスであり、ローカルにしたところで単に名前空間やアクセス制限が加わるだけに過ぎません。例えば、C++ で上の様なコードを書くと、moveDown などの TestApplet 内のメンバを呼ぶ事ができないわけです。
これを回避するには、このようなローカルクラスに親クラスへの参照(ポインタ)を渡し、その参照を通して親クラスのメンバにアクセスする必要があります。
class Test { ... 略 ... friend class Control; class Control : public KeyAdapter { private: Test& m_obj; public: Control(Test* obj) : m_obj(*obj) { } void onKeyDown(UINT code, UINT repeat, UINT flags) { switch(code) { case VK_DOWN : m_obj.moveDown (); break; case VK_LEFT : m_obj.moveLeft (); break; case VK_RIGHT: m_obj.moveRight(); break; } } }; }; |
別の実装法として、KeyAdapter を Test に直接継承させることも考えられます。この場合は実装は楽になりますが、1クラスにつきイベントハンドラを1つしか作る事ができません。状況に応じてイベントハンドラを変更するなどといった事を行いたい場合にはローカルクラスを作るか、Test に相当するクラスごとすげ替えてやる必要があります。
C# においては、イベントハンドリングにはデリゲートというものが利用されます。デリゲートというのは、処理と、その処理を受けるオブジェクトとをカプセル化したものです。C++ でいう関数ポインタのパワーアップ版と考えてもらって構いません。例えば、次のプログラムを見て下さい。
using System; delegate void Foo(); public class Test { static public void Main() { Foo m_foo; Test test1 = new Test(1); Test test2 = new Test(2); m_foo = test1.hoge; m_foo(); m_foo += test2.hoge; m_foo(); } int m_n; Test(int n) { m_n = n; } void hoge() { Console.WriteLine("{0}", m_n); } } |
この Foo というのがデリゲートです。
delegate void Foo();
これは、C++ でいうと、関数ポインタ型を typedef したようなものだと考えて構いません。こうして Foo m_foo; としてやると、m_foo がデリゲートオブジェクトになります。デリゲートオブジェクトにオブジェクトとメソッドの対を代入してやると、
m_foo = test1.hoge;
デリゲートオブジェクトを使って関数を呼ぶ事が出来ます。
m_foo();
ここで注目すべき事は以下の2点です。
1. は要するに、メンバ関数ポインタで必要だった void (Test::*)() の Test:: が必要ないだけでなく、どんなクラスのメソッドも(静的なメソッドも)代入できるということです。これはイベントハンドリングにおいて非常に重要な事ですが、詳細はまた後で述べます。
2. は言葉のままです。メンバ関数ポインタを使う場合は呼ぶ時にオブジェクトを指定しますが、デリゲートの場合はメソッドと一緒にオブジェクトも同時に登録するため、呼び出す時にオブジェクトをいちいち指定する必要はありません。
デリゲートの面白い所は
m_foo += test2.hoge;
と += で足し込んでやる事により、複数のメソッドを連続して実行する事ができる所です。この状態で
m_foo();
とすると、test1.hoge と test2.hoge の両方が呼ばれることになります。m_foo -= test1.hoge; とすれば、test1.hoge を呼ばないようにする事も出来ます。
話を元に戻しますが、C# におけるイベントハンドリングにはデリゲートが使われます。どうするかは非常に単純で、イベントを表すデリゲートオブジェクトがあり、そこにオブジェクトとメソッドを代入してやればいいだけです。Java の時と違って、イベントハンドラを作るのにいちいち派生/実装を行う必要もなければ、メソッドの名前も自由です。普通にメソッドを作り、ただそれをデリゲートオブジェクトに代入すればいいだけなのです。複数のイベントハンドラを作る事も自由で、複数同時に登録して連続実行することもできるわけです。
できることは Java と同じではあるのですが、実装が随分簡単になってることが分かると思います。
デリゲートを C++ で実装するためにはどうすればいいでしょうか? 先ず、デリゲートはオブジェクトを保持していることから、
template <typename Class> class Foo { private: typedef void (Class::*Fn)(); Class* obj; Fn fn; public: Foo() : obj(NULL), fn(NULL) { } Foo(Class* obj, Fn fn) : obj(obj), fn(fn) { } void operator()() { (obj->*fn)(); } }; |
としてみましょう。とりあえずクラスを問わないようにするためテンプレートにしています。しかし、これだけではデリゲートとは言えません。なぜなら、
Foo<Test> foo;
のように、デリゲートオブジェクトが特定のクラスに依存してしまうからです。イベントハンドリングに利用するためには未知のクラスのメソッドを代入できなくてはならないので、このように特定のクラスに依存してしまっては使い物にならないのです。
とはいえ、これを回避するのは実は簡単です。基底クラス IFoo を提供してやればいいだけです。
class IFoo { public: virtual ~IFoo() { } virtual void operator()() const = 0; }; template <typename Class> class CFoo : public IFoo { public: typedef void (Class::*Fn)(); private: Class* obj; Fn fn; public: CFoo(Class* obj, Fn fn) : obj(obj), fn(fn) { } virtual void operator()() const { (obj->*fn)(); } }; #ifdef __GNUC__ #define TYPENAME typename #else #define TYPENAME #endif template <typename Class> IFoo* Foo(Class* obj, TYPENAME CFoo<Class>::Fn fn) { return new CFoo<Class>(obj, fn); } |
これを例えば次のように使えば、
class Base { private: auto_ptr<IFoo> m_foo; public: void attach(IFoo* foo) { auto_ptr<IFoo> tmp(foo); m_foo = tmp; } void call() { (*m_foo)(); } }; class Test : public Base { public: void run() { attach(Foo(this, &Test::hoge)); call(); attach(Foo(this, &Test::bar)); call(); } void hoge() { cout << "hoge" << endl; } void bar() { cout << "bar" << endl; } }; int main() { Test().run(); return 0; } |
デリゲートっぽい動作になります。
デリゲートオブジェクトは Foo 関数で生成し、Base クラスのスマートポインタ auto_ptr<IFoo> で保持するようにします。デリゲートは attach 関数で登録し、call 関数で呼び出すようにしてあります。このデリゲートは実際には Base の派生クラス Test から登録します。ここで注目する事は、Base のデリゲートに関する処理は特定のクラスに依存していないことです。
実際にデリゲートに登録しているのは Test::run です。最初の attach では this->hoge を登録しています。
attach(Foo(this, &Test::hoge));
Foo 関数は CFoo<pTest> のオブジェクトを new して返す関数です。ここで初めて特定のクラスに依存したクラスが登場するわけですが、基底クラスの IFoo* にキャストして扱うため、その依存性が隠されるのです。
そして call 関数を呼ぶと、実際に this->hoge が呼ばれるわけです。call 内で呼ばれる operator() は仮想関数になっており、実際に呼ばれるのは CFoo<Test>::operator() になります。
あとは複数登録や削除などを加えてやれば本物のデリゲートっぽくなります。一応ここにその実装例を置いておきますが、実際にはここまでデリゲートっぽくするよりは、上記の簡易デリゲートを状況に応じて工夫しつつ使用した方が実行効率や利便性は上がると思います。
Windows プログラミングでは、ウィンドウメッセージに対する操作を行う必要があります。MFC のようにメッセージハンドラを仮想関数を使って実装しても別に構わないのですが、デリゲートを使って実装する事もできるわけです。MFC ではウィンドウクラスのメンバとして唯一のメッセージハンドラを作成することしかできませんが、デリゲートを使った場合は複数のメッセージハンドラを使う事ができ、さらにメッセージハンドラはウィンドウクラス(プロシージャクラスとして分離されてるかもしれませんが)以外のクラスのメンバ関数を使う事もできるのです。
他にも、通常のメッセージハンドラは MFC と同様に仮想関数で扱い、独自のウィンドウメッセージに対するハンドラやコマンドハンドラにはデリゲートを使う、という実装も考えられるかと思います。
タスク処理というのは、よくゲームで使われる機構です。一連の処理を登録しておき、それを一気に実行するというものです。その処理単位の事をタスクと呼びます。
Windows プログラミングでは例えば
MSG msg; DWORD prev = timeGetTime(); DWORD span = 1000/60; for(;;) { if(PeekMessage(&msg, NULL, 0, 0, RM_REMOVE) { if(msg.message == WM_QUIT) { return msg.lParam; } TranslateMessage(&msg); DispatchMessage(&msg); } else { DWORD now = timeGetTime(); if(now - prev > span) { do { prev += span; prepare(); } while(now - prev > span); draw(); } Sleep(0); } } |
のようにし、ウィンドウメッセージの来てない時に、一定時間(span)毎に実行される prepare, draw の部分で処理を行います。prepare では描画以外の処理を行い、draw で描画処理を行います。
prepare や draw の処理は、状況によって様々に変化します。例えば画面上に新しいキャラが登場すれば、新しいアニメーション処理とその描画処理が追加されますし、キャラが退場すればその処理は必要なくなります。このように様々な処理を追加したり削除したりする場合には、タスク処理が非常に役に立ちます。
もうお気づきでしょうが、このタスク処理はデリゲートでやっていることと全く同じです。タスクを普通のメンバ関数として実装し、それをデリゲートに登録したりデリゲートから削除したりしてやればいいわけです。細かいタスク制御を行いたい場合は、C# のデリゲートと同じ様な仕様にするよりは、もっとタスク処理に最適化した独自の実装にした方がいいと思います。
デリゲートの応用はまだまだあると思います。C++ でデリゲートを実装するのは少し面倒な部分も多いのですが、それでも実装するだけの価値があると思います。
Last update was done on 2005.4.15