Listener か function pointer か
アセンブリやC言語の場合、モジュール化したプログラムの特定の状態で通知や処理の移譲を受けるために関数ポインタを利用する。このテクニックはC++でも変わらず使われている。関数ポインタと、ペアになる void* アドレスの登録を受け付ければ大抵の状況に対応できる。また、このペアを複数登録するための(単方向の)リストコンテナは1要素あたり僅か12バイト程度なので、必要に応じて比較的気楽にコールバックの複数登録を受け付けることができる。
C++ではクラスが使用できるため、 OGRE3D でも好まれている Listener クラスを使って、もう少し "それっぽく" まとめることができる。
class CEnemey { // // .... // public: class Listener { public: Listener() {} virtual ~Listener() {} virtual bool notifyDamaged(const CEnemy*) = 0; virtual bool notifyKilled(const CEnemy*) = 0; virtual bool notifyDestructed(const CEnemy*) = 0; }; /// \a pListener を通知リストに加える void addListener(Listener *pListener); /// \a pListener を通知リストから抜く void removeListener(Listener *pListener); };
この実装方法には、個人的に次のようなメリットがあると考えている:
- void* の static_cast が減る。
- 既存のクラスが Listener クラスを implement して、そのまま対象に addListener(this) できる。
- 監視専用サブクラスのインスタンスを作って addListener することもできる。
たとえばチームリーダーが殺されたら一目散に逃げる AI に切り替わるような敵は(C++で実装するのはどうかとも思うが、例えとして)、 CEenemy::Listener を実装して、チームリーダーに addListener(this) できる。
他方、ゲームスクリプトから、特定の敵が特定の状態になったときにスクリプト内定義のユーザー関数を呼ぶといった処理を実装する場合は、リスナー機能だけのサブクラスを作れば済む。
class CEventListener : public ::CEnemy::Listener { // // .... // public: CScriptEventListner(...); ~CScriptEventListener() {} bool notifyDamaged(const CEnemy*); bool notifyKilled(const CEnemy*); bool notifyDestructed(const CEnemy*) {} };
……と一見スマートにいきそうなのだが、実はこの方法、「このリスナーだけの存在(インスタンス)を誰が delete するのか」という点で、非常に大きな問題を抱えている。もし、
{
CEventListener *pListener = new CEventListener();
pEnemy->addListener(pListener);
}
……なんてコードを書いたらメモリリークになる。 delete this でも使わない限り notify 後にこのインスタンスをメモリ上で始末するにはメインシステム側に何らかの仕組み(たとえばタスクシステムのような)が必要になってしまう。
ひとつの解法はスマートポインタを使うことだが、リスナー周りの API を主に使うのが自分なのか、他人なのかで判断が変わってくる。今回のようなケースでは、 addListener(this); が示すように、リスナー受け入れをスマートポインタに限定すると周りの人にとって一気に使いにくいものになってしまう。
関数ポインタだと何の問題も発生しない。
static bool XXXX_notifyKilled_callback(const CEnemy *pEnemy, void *ptr) { // 中継する CScriptEventListner *pListener = static_cast<CScriptEventListener*>(ptr); pListener->notifyKilled(pEnemy); delete pListener; // 用済みなので消す(これが可能になる) return false; // もう通知を受信しない }
Listener クラスを用いたデメリットとしてはこんなところだろうか:
- void* の static_cast は減るが、リスナー単体のインスタンス生成が増えるのでメモリリークに要注意(誰が delete するのか)
- メモリ効率において、スリムな関数ポインタと比較して、 vtable ぶん肥満する。(全ての通知仮想関数を実装したときのみ関数ポインタと同等の効率になる)
- みんなで話し合ってると「どう考えても関数ポインタを使う方がスマート」という結論に達する。(^^;
タスクシステムやジョブシステムに相当するものがあれば、生成したリスナー単体のインスタンスもどこかのコンテナに格納して外から delete してもらえるので心配が要らない。もしそういう統合的なマネージャがないなら、あるフラグが立っていたときに Observer が delete してくれるという組み方を作っておくのも手だと思う(若干危ういが)
"本当にきちんと" やろうとすると、リスナーが孤児にならないように管理の仕組みをしっかり作ることになる。しかし、いちいちそんなことするならやっぱり関数ポインタでいいやってなってくる。
(そういう意味では汎用のタスクシステムがあれば、こういうケースでは最強)
ちなみに、デメリットにメモリ効率について書いたが、据え置き機 + PSP ではまず誤差範囲だろう。