クラス内メソッド遷移からswitch〜caseを消すMethodExecテンプレート
例えばRPGの戦闘シーンを演出するCBattleクラスがあったとします。このクラスはシーン演出のために細かな遷移があり、それをメソッド呼び出して解決しているとしましょう。実装例はこんな感じです:
class CBattle
{
public:
void exec(); // バトルエントリ
private:
void init(); // バトル初期化
void loadResource(); // リソースロード
void setPlayerMenu(); // プレイヤーの行動選択
void setEnemyMenu(); // 敵の行動選択
void startBattle(); // バトルスタート
void calcDamage(); // ダメージ計算
void resultBattle(); // バトル結果反映
void afterBattle(); // バトル終了時処理
};
CBattleオブジェクトの呼び出し側はCBattle::execメソッドを毎回呼び続けます。すると、CBattleオブジェクト内で戦闘が遷移しながら勝手に進むというからくりを考えています。
遷移させるための、CBattle::execメソッドの実装の典型は次のような感じでしょう:
void CBattle::exec() {
switch( proccess_ ) {
case Battle_Init:
init();
break;
case Battle_SetPlayerMenu;
setPlayerMenu();
break;
case Battle_SetEnemyMenu:
setEnemyMenu();
break;
...
}
}
現在のプロセスをproccess_という変数に持たせて、そこに飛ぶ仕組みです。各呼び出しメソッド内でproccess_を書き換えれば、クラス内遷移が実現できます。
先にフォローを入れておきますと、この方法は良く使われますし、決して悪いというわけではないと思います。可読性もそれほど悪くなく、デバッグも追いやすい。でも、一つ難を挙げるならば、実装が面倒です。まず「Battle_Init」などproccess_変数に与える状態変数(列挙型)を作らないといけません。さらに、execメソッド内のswtich〜caseを追加して、そこにメソッドを呼び出す記述とbreakを忘れずに書き加える必要があります。これ、面倒なんです。列挙型の示すものは、関数の名前そのものでしょうし、それをわざわざswitch〜caseで確認させるのも遠回りです。新しく遷移を付けたい時に、この作業は正直煩わしいものがあります。
この章の目的はこの実装をもっとすっきりさせて、最終的にexecメソッド内からswitch〜caseを消す事にあります。そのためにMethodExecテンプレートと言うのを作成します(テンプレート名は私が勝手に付けたもので一般的ではありません)。このテンプレートをちゃんと理解して実現するためには、「関数ポインタ」まで話を戻す必要があります。ちょっと長くて退屈な章かもしれませんが、その分テンプレートの恩恵は凄まじいので、どうぞ飽きずに最後までご一読下さい。ではいつものように、コッテリと参ります(^-^;。
@ C言語における関数ポインタベースの遷移
クラスを言語サポートしていなかったC言語では、関数はすべてグローバルな領域に書いていました。先程の戦闘に使うメソッドをC言語でプロトタイプ宣言するとこうなります:
void battleExec(); // バトルエントリ関数
void init(); // バトル初期化
void loadResource(); // リソースロード
void setPlayerMenu(); // プレイヤーの行動選択
void setEnemyMenu(); // 敵の行動選択
void startBattle(); // バトルスタート
void calcDamage(); // ダメージ計算
void resultBattle(); // バトル結果反映
void afterBattle(); // バトル終了時処理
この遷移を実現するために、1つの関数ポインタ変数を用意します:
void (*BattleFunc)() = init;
ここではinit関数へのポインタを入れて変数を初期化しています。関数ポインタの書き方にまだ戸惑いがある方は、次の2つを比較されると良いかもしれません:
// float型の戻り値で引数がint型1つという形の関数ポインタを格納できるBattleFunc変数
float (*BattleFunc)( int a );
// float型へのポインタを返して引数がint型であるBattleFunc関数
float *BattleFunc( int a );
下のように括弧が無いとポインタを返す関数だと思われてしまうので、上の記述のような括弧が必要になるわけです。これは何度か使うと慣れます。
関数ポインタであるBattleFunc変数にプロトタイプ宣言した関数へのポインタを入れ替えていくと、同じ呼び出しで違う関数が呼ばれる、いわゆる「遷移」が実現できます:
void init() {
BattleFunc = loadResource; // リソースロードに遷移
}
void loadResource() {
...
}
void (*BattleFunc)() = init; // init関数に初期化
main() {
while(1) {
BattleFunc(); // 関数ポインタを実行する時は通常の呼び出し方に直せばOK
}
}
BattleFunc変数はinit関数で初期化されているので、最初はinit関数が呼ばれます。init関数内ではloadResource関数にスイッチしているのがわかりますね。ですから、次の呼び出しではloadResource関数が呼ばれるわけです。こんな感じでC言語の場合はわりとわかりやすくゲームの遷移が実現できるのでした。
ただ、このC言語な実装方法には変数の保護や関数呼び出しの保護といった概念が無いので、複雑になるとだんだんワケがわからなくなってきます。大規模なゲームになってくると、これでは保守が難しくなってしまうんです。ある遷移を一まとめにして保護したい。ある遷移内だけしか使わない変数を管理したい。そうなってくると、「クラス内遷移」が登場となります。
A クラスではC言語と同じ事ができない!!
手厚い保護と遷移のパッケージ化をはかるために、先ほどのC言語版遷移をクラス内で実装(クラス内遷移)してみます。仮組みするとCBattleクラスの宣言部はこうなるでしょうか:
class CBattle
{
public:
void exec(); // バトルエントリ
CBattle(); // コンストラクタ
private:
void init(); // バトル初期化
void loadResource(); // リソースロード
void setPlayerMenu(); // プレイヤーの行動選択
void setEnemyMenu(); // 敵の行動選択
void startBattle(); // バトルスタート
void calcDamage(); // ダメージ計算
void resultBattle(); // バトル結果反映
void afterBattle(); // バトル終了時処理
private:
void (*BattleFunc)(); // バトル遷移関数ポインタ
};
コンストラクタとメソッドへのポインタを保持するBattleFuncメンバが追加されています。一番最初に呼び出されるメソッドをコンストラクタで登録して、後はBattleFuncに登録していけば、execメソッド内はこんなイメージで実装されそうです:
void CBattle::exec {
BattleFunc();
}
何だ簡単、すっきりです。何よりこれでswitch〜caseが消えました。でも、「めでたしめでたし、この章はこれでおしまい!」…というわけでは、残念ながらやっぱりないんです(^-^;。そこにはC++特有の壁があり、それを取り除くために長〜い理屈が必要になります。
さっそくその「壁」にぶち当たってみます。C言語の時と同じように、コンストラクタでinit関数へのポインタを登録します:
CBattle::CBattle() {
BattleFunc = init;
}
見た目は問題ありませんね。ところが、実はこのコンストラクタの実装はコンパイラを通りません!C言語とこの実装と、何が違うのでしょうか?
C言語の関数は、すべてグローバルな領域にありました。グローバル領域にある変数や関数は、その配置されるメモリ領域がコンパイル時に確定します。こういうのを「静的(static)」などと呼びます。一方クラスのメソッドはstatic宣言しない限りはすべて「非静的(non-static)」として扱われます。非静的な変数やメソッドは「オブジェクトが作成されるまでそのメモリ位置を決められない」という性質を持ちます。
例えるなら、静的な関数は「北海道旭川市□□町○条△丁目IKD様」のように住所が固定している状態なので、郵便屋さん(関数ポインタ)は住所録(コンパイル後実行ファイルに記録されている位置)を見て手紙をちゃんと届けてくれます。しかし非静的な関数は「○条△丁目IKD様」という部分(クラスメソッド)しかなくて、肝心の「北海道旭川市」とか「京都府東山区」「東京都新宿区」のように住所確定する部分(=オブジェクト)がありません。そのまま住所録に載せると、郵便屋さんが手紙の届け先を確定できない危険があるため、「どちらのIKD様か確定していませんよ!」とエラーを出すわけです。
実は私たちが何気なく使っている、
CBattle btl;
btl.exec();
という呼び出しは、言ってみればbtl(=北海道旭川市□□町)+exec(○条△丁目IKD様)という合わせ技でメソッドの実行位置(処理の飛ぶ先)を「確定」させているわけなんです。この壁を突破しないと、ここでのクラス遷移は実現できません。さてどうしようです。
B クラスメソッドは保持できる、けど実行は出来ない
実は、クラスメソッド自体は位置が確定しています。「???」と混乱させてしまったらごめんなさい。クラスメソッドというのは、
CBattle::exec();
のように「クラス名::メソッド」と表記され、クラスが持つメソッドを直接実行する物です。クラスのメンバメソッドは「仮住所」のような位置を持っていて、上のようにするとその仮住所が指定されます。住所不定ではないので呼び出し可能なんです。ですから、
void (CButtle::*ButtleFunc)() = CBattle::exec;
と「クラス名::メソッド名」という並びでクラスメソッドへのポインタを指定すると、その関数ポインタを保持する事ができます。ただし、これはpublic宣言されたメソッドのみですからご注意を。もう一つ注意を。上の実装はVisual Studio 2003までは合法でしたが、Visual Studio 2005ではコンパイラの改正がありまして、このままだとC3867コンパイルエラーとなります(ISO C++ 標準に違反)。コンパイラを通すには「&」を付けて、
void (CButtle::*ButtleFunc)() = &CBattle::exec;
とします。どちらにしても、クラスメソッド(クラスメンバ)は上のような実装で保持する事ができるわけです。
「おお、じゃぁBattleFuncを実行したらどうなるの?」となりますが、もちろん実行できません。保持するだけならコンパイルは通るのですが、
ButtleFunc();
と実行すると、次のような文句を言われます。
C2064エラー error C2064: 0 引数を取り込む関数には評価されません。
「え?いや、その通りで引数が無いメソッドだけど…?」と思われるかもしれません。ここで言っている引数というのは、通常の引数ではなくて「thisポインタ」の事なんです。
先ほど、
CBattle btl;
btl.exec();
という呼び出しをしましたが、実はこの時に暗黙的にexecメソッドにbtlを指すthisポインタが渡されています。どうしてそんな事をするのか?これはオブジェクトを指す「this=北海道旭川市□□町」だからなんです。execメソッドは「○条△丁目IKD様」でした。thisをexecメソッドにこそっと渡す事により、execメソッドの位置が「北海道旭川市□□町○条△丁目IKD様」という住所にここで初めて確定するんです。このことから、thisポインタを渡す事は極めて大切でして、コンパイラがプログラマに代わって確実にその仕事を裏でしてくれているわけなんです。
ところが、CBattle::ButtleFunc()という呼び出しには持ち主たるthisがいません(クラス自体はオブジェクトではないのでthisはない)。住所不定な呼び出しなので「住所は?(引数thisは?)」とコンパイラに聞かれた、というカラクリだったというわけです。
C こんなのあり?の住所確定方法
C++では住所をthis部分とメソッド部分に分けていて、実行するにはその住所を組み合わせなければならない事を説明してきました。となりますと、こんな形で両方を用意するとどうなるのでしょうか?
CBattle btl; // ○条△丁目IKD様
void (CBattle::*ButtleFunc)() = CBttle::exec; // 北海道旭川市□□町
両方揃っています。thisを渡すには、いつもの書き方をすれば良いわけでして、上の2つを何とか組み合わせてみたいわけです。実は、ここで両者をがっちりとつなぐ神業のような実装方法があります!次をご覧下さい:
メンバ関数ポインタ演算子による間接的なメンバの実行 (btl.*ButtleFunc)();
「なんじゃこれ〜」です。「.*」は「メンバ関数ポインタ演算子」というC++の演算子の一つです。BattleFuncにはCBattle::execクラスメソッドへのポインタを格納しました。ポインタ変数の前に「*」を付けると、その参照先を示してくれます。これは関節参照演算子と同じ役目をしているわけです。BttleFuncが指す先にはexcメソッドがありますから、上のソースは「btlオブジェクトが持つ、BattleFuncが指しているexecクラスメソッドを呼び出してね」という宣言になっているんです。btlというオブジェクトを通しているので、もちろん*BattleFuncメソッドにはthisポインタも渡されます。
初めてこれを知った時には、私はちょっとたまげました。クラスのインスタンスとクラスのメソッドは、実はこんな形で結びついているんだなぁとしみじみ感心したのを覚えています。ちなみに、btlがポインタの場合は「->*」というメンバ関数アロー演算子が使われます。
D メンバ関数ポインタ演算子を使った遷移テスト
メンバ関数ポインタ演算子(メンバ関数アロー演算子)を使って、遷移に繋がる簡単なテストイメージ関数を示しておこうと思います。これがないと最後のテンプレートクラスがきっとちんぷんかんぷんです。
まず、グローバルな領域にBattleExecという関数を作ります。実装は次の通りです:
// メンバ関数を間接的に実行する関数
void BattleExec( CBattle* obj, void ( CBattle::*Func)() ) {
(obj->*Func)();
}
この関数の目的は、第1引数のオブジェクトが持つ第2引数のメソッドを実行することです。今までの説明を噛み砕きながら、よ〜くご覧下さい。これにより、CBattleクラスが持つ、
public:
void Hoge();
などの第2引数と同じ型を持つメソッドを呼び出して実行する事ができます。「面白いけど、これはいったい?」かもしれません。ここで注目したいのは、関数の引数にあるクラス型です。今は第1第2引数共に「CBattle」になっていますが、これ、別に他のクラスでも別に良いわけです。つまり任意型にできる。任意型というと、そうテンプレートです。
イメージとして、
template <typename T>
void BattleExec( T* obj, void ( T::*Func)() ) {
(obj->*Func)(); // T型のobjオブジェクトが持つメソッドを実行
}
とテンプレート引数(T型)に置き換えてもいいわけですね。何か、見えてきました(^-^)。
E MethodExecテンプレート登場
ようやくこれでMethodExecテンプレートの話につながります。MethodExecテンプレートは上のような任意のクラスが持つ特定メソッド型を実行するテンプレートです。上の理屈にならってクラスを実装するとこうなります:
MethodExecテンプレート template <typename T>
class MethodExec {
public:
MethodExec( MethodExec<T> (T::*f)() ) : func_( f ) {};
MethodExec<T> exec( T* obj ) {
return (obj->*func_)();
}
private:
MethodExec<T> (T::*func_)(); // 実行するT型クラスのメソッドポインタ
};
コンストラクタでは例えば、
MethodExec<CBattle> MEB( CBattle::exec );
のようにクラスメソッドを渡します。ただし、ここで渡すメソッドは必ずMethodExec<T>型の変数を返す必要があります。これが遷移させるポイントなんです。引数に渡されたクラスメソッドポインタは瞬間func_という変数に格納されます。
メソッドを実行するにはMethodExec<T>::execメソッドにオブジェクトを渡します。
CBattle btl;
MEB.exec( &btl );
これでbtl.exec()を実行したのと同じ意味になります。
コンストラクタでクラスメソッドへのポインタ(○条△丁目IKD様)、そして実行時にオブジェクトへのポインタ(北海道旭川市□□町)を渡す事により、次のようにクラス内遷移が実現できてしまいます:
クラス内遷移ができるCBattleクラス(Visual Studio 2005対応) class CBattle {
typedef MethodExec<CBattle> MEB;
public:
MEB onExec_; // MethodExecオブジェクト
public:
CBattle() : onExec_( &CBattle::init ) {
}
void exec() {
onExec_ = onExec_.exec( this );
}
private:
MEB init() {
return &CBattle::loadResource; // リソースロードへ遷移
}
MEB loadResource() {
return &CBattle::setPlayerMenu; // プレイヤーのメニュー選択へ遷移
}
MEB setPlayerMenu() {....};
};
コンストラクタでMEB(MethodExec<CBattle>のtypedef)型のonExec_という変数を初期化しています。クラスメソッドinitを渡しているのがわかりますね。もちろんクラスの中なのでprivate宣言されていてもアクセスできます。CBattleオブジェクトの呼び出し元は、ゲームループで毎回CBattll::execメソッドを呼び出します。execメソッド中では、
onExec_ = onExec_.exec( this );
とMEB::execを呼び出しています。thisはオブジェクトのポインタで、これは実行時に確定している自分自身へのポインタなので合法です。このthisと、コンストラクタで登録したクラスメソッドが内部で手を結び、めでたく位置が確定したメソッドが実行される事になります。そして、各メソッドは実行後にMEBオブジェクトを返してくれます。
例えばinitメソッドを見てみましょう。これが実行された後はCBattle::loadResourceクラスメソッドを返していますよね。これがonExec_に代入(登録)されるので、次のexecメソッドの呼び出しではloadResourceメソッドが呼び出されるわけです。どうでしょう!クラス内遷移がメソッドの戻り値だけで実現できてしまっています!!もちろんswitch〜caseはどこにもありませんし、煩わしい列挙型の定義もありません。ただ遷移に関係するメソッドがMethodExecテンプレートのオブジェクトを返すだけで、遷移の矢印が繋がるんです!!!
文章だらけの章でした。私も読み返して「やれやれ」ですから、皆さんはもっと大変です(^-^;。ですが、このテンプレートをクラスの中に組み込む事で、クラス内でC言語の関数ポインタのように自由に実行を遷移させる事ができます。これはあるシーンをクラスで再現する時などに絶大な威力を発揮するはずです。しかも、シーンで使われる変数はクラスの囲いでがっちり保護され、外側のオブジェクトはクラスのprivateメソッドを呼び出せないので、遷移の状態も保護されます。
MethodExecテンプレートは上のソースをコピペすると直ぐに使えますので、どうぞご自由にご利用下さい。お疲れ様でした。