その8 引数型が違う状態メソッドでも遷移ができる状態遷移テンプレート
前章その7ではゲーム遷移を実装するためにクラス内に「ステートメソッド(状態メソッド)」を設けて、関数ポインタを通して実行する方法を導いてきました。ちょっとおさらいとしてソースをご覧下さい:
GameScene.h class GameScene {
public:
GameScene();
~GameScene();
private:
void demo(); // デモ
void title(); // タイトル
void game(); // ゲーム
public:
//! 更新メソッド
StateResult update() {
( this->*func_ )();
return stateResult_;
}
private:
StateResult stateResult_; // 終了フラグ
DemoScene demoScene_; // デモ
TitleScene titleScene_; // タイトル
MyGameScene mygameScene_; // ゲーム
void( GameScene::*func_ )(); // メソッドポインタ
};
func_というメンバ変数がメソッドポインタになっています。ここにデモ、タイトル、そしてゲームのメソッド(demo、title、gameメソッド)のポインタを登録すると、updateメソッド内の赤い部分で示した所でそのメソッドが実行されると言う仕組みになっています。
これはswitch〜case文やステート変数を設ける事無く遷移ができるので私は結構好きなのですが、1つ不満もあるんです。それは、func_変数に渡せるメソッドポインタは「void func()」という型に限定されてしまうという点です。例えば、先ほどのステートメソッドが、
遷移するメソッドの型が違う… private:
void demo( int mode ); // デモ
void title( char* caption ); // タイトル
void game( int mode, GAMELEVEL level ); // ゲーム
のような感じだったら、どれもfunc_変数には代入できません。これは、やっぱり微妙に不満なわけです。
そこで、この章では上のような様々な引数型を持つ状態遷移メソッドを1つの変数に保持できるマルチ状態遷移テンプレートを作ってみる事にしました。前章と同様に叩き台から少しずつリファクタリングしていきます。少し長い章ですが、最初の酷いたたき台がテンプレートに発展してスマートに変貌して行く様子は中々にして見ものですよ(^-^)。
@ まずは酷いたたき台から
前章と同様に、冒頭のような型が違う遷移メソッドを遷移させる方法を実装してみます。条件としてメソッドポインタを通します。無理っくりにやるとこうなります:
たたき台実装 class GameScene {
private:
// ステートメソッド
void demo( int mode ); // デモ
void title( char* caption ); // タイトル
void game( int mode, GAMELEVEL level ); // ゲーム
public:
//! 更新メソッド
StateResult update() {
switch( flag_ ) {
case 0:
( this->*demo_ )( mode_ ); break;
case 1:
( this->*title_ )( caption_ ); break;
case 2:
( this->*game_ )( mode_, level_ ); break;
}
return stateResult_;
}
private:
StateResult stateResult_; // 終了フラグ
// メソッドポインタ
void( GameScene::*demo_ )( int );
void( GameScene::*title_ )( char* );
void( GameScene::*game_ )( int, GAMELEVEL level );
int flag_;
int mode_;
char* caption;
GAMELEVEL level;
};
もちろん、酷すぎです(笑)。updateメソッド内で遷移を起こすためにswitch〜caseが復活しました。関数ポインタを通したステートメソッドを呼び出す必要があるため、flag_変数で分岐させています。その関数ポインタもメソッドと同じな前になっていて2重化状況になっています。各ステートメソッドに引数がありますが、それが実行されるタイミングがupdate時になるので、引数の値を一時的に取っておく必要も出てきました。そのためクラスのメンバにmode_とかlevel_などが設けてあります。酷すぎです(^-^;
ここから、これをたたき台に超ガッツリリファクタリングしていきます。…の前に、この章の最終結果をまずはご覧頂きたく思います。この章の最終段階で、上のソースは何とこうなります!
最終実装はこうだ! #include "FuncSetter.h"
class GameScene {
private:
// ステートメソッド
void demo( int mode ); // デモ
void title( char* caption ); // タイトル
void game( int mode, GAMELEVEL level ); // ゲーム
public:
//! 更新メソッド
StateResult update() {
funcSetter_.exec();
return stateResult_;
}
private:
StateResult stateResult_; // 終了フラグ
FuncSetter_< GameScene > funcSetter_; // ステート管理オブジェクト
};
funcSetter_というテンプレートオブジェクトにより、引数型が全然違う3つのステートメソッド間でちゃんと状態遷移をするようになります。updateメソッド内からはswitch〜case文が消え、クラス内にも引数を保持する余計なメンバ変数はありません。ちなみに、ステートメソッドの事前登録のような作業も一切必要としません。特定のステートメソッド内で次のメソッドに遷移させるコードはこの1行です:
ステートメソッドの変更 funcSetter_.setFunc( &GameScene::game, 2, GAMELEVEL_DIFFICULT );
実はここにすべてがぎゅ〜〜〜っと凝縮されています。setFuncメソッドの第1引数にメソッドへの参照、第2引数以降にメソッドの持つ引数を渡します。凄まじいのが、上のsetFuncメソッドは引数を2つとるクラス内のメソッドであれば何でも受け付けます。もちろん、setFuncメソッドは引数が1つ、引数が無いメソッドもちゃんと受け付けます。この1行を書くだけで、次のupdateメソッド内でgame( 2, GAMELEVEL_DIFFICULT )が実行されます。呼び出しと実行に遅延を作れる魔法のようなテンプレートなんです。
それでは、この採集形態に向かってリファクタリングを始めましょう。
A 引数とメソッドをパックにできないか?
最初の駄目駄目な遷移の実装には、まずい部分が沢山あります。その1つが「ステートメソッドの引数をクラスのメンバとして全部取っておかなければならない」ところです。ステートメソッドの変更を宣言してからupdateメソッド内でステートメソッドが実行されるまで遅延があるため、この保持が必要となっているわけですが、これはかなりに嫌です。
いきなり綺麗さっぱり消すのは至難の業なので、とりあえずの通過点として呼び出すメソッドと使う引数の値をパックにしてまとめてしまいましょう。例えばこんな感じになります:
ステートメソッドの引数とメソッドポインタをパックする class GameScene {
private:
// int型引数を持つメソッド
struct func_Int {
void ( GameScene::*func_ )( int ); // メソッドポインタ
int arg_; // 引数
void exec( GameScene *pObj ) {
( pObj->*func_ )( arg_ );
}
}:
// char*型引数を持つメソッド
struct func_CharPtr {
void ( GameScene::*func_ )( char* );
char* arg_;
void exec( GameScene *pObj ) {
( pObj->*func_ )( arg_ );
}
};
// int型とGAMELEVEL型を持つメソッド
struct func_Int_GAMELEVEL {
void ( GameScene::*func_ )( int, GAMELEVEL );
int arg0_;
GAMELEVEL arg1_;
void exec( GameScene *pObj ) {
( pObj->*func_ )( arg0_, arg1_ );
}
};
private:
func_Int demo_;
func_CharPtr title_;
func_Int_GAMELEVEL game_;
int flag_;
public:
StateResult update() {
switch( flag_ ) {
case 0:
demo_.exec( this ); break;
case 1:
title_.exec( this ); break;
case 2:
game_.exec( this ); break;
}
return stateResult_;
}
};
メソッドポインタとその引数をパックにした構造体を定義して、その中に「exec( this )」という共通のメソッドを用意します。このメソッド内で持っているメソッドに引数を渡して実行します。こうすることでupdateメソッド内はちょっと整理されます。先ほどまで乱雑だった呼び出しが全く同じexecメソッドに統一されました。
さて、メソッドが同じ…と聞くと、何だかむずむずしてくるわけです。そう「仮想化」です。上の各構造体内のexecメソッドは、親クラスで仮想化することでポインタを通して呼び出しを振り分けられます。そこで次のように改善します:
execメソッドを親クラスで共通化 class GameScene {
private:
class FuncExecutor {
public:
virtual void exec( GameScene* pObj ) = 0; // 共通呼び出しメソッド
};
// int型引数を持つメソッド
class func_Int : public FuncExecutor {
public:
void ( GameScene::*func_ )( int ); // メソッドポインタ
int arg_; // 引数
virtual void exec( this ) {
( this->*func_ )( arg_ );
}
}:
... ちょっと省略 ...
private:
func_Int demo_;
func_CharPtr title_;
func_Int_GAMELEVEL game_;
int flag_;
FuncExecutor* stateMethod_; // 実行するステートメソッド
public:
StateResult update() {
stateMethod_->exec( this );
return stateResult_;
}
void demo( int mode ) {
title_.arg = Opening_Message_g; // 文字列へのポインタを代入
stateMethod_ = &title; // タイトルに変更
}
};
親クラスの派生なので、構造体をクラスにランクアップさせました。title_オブジェクト(func_CharPtrクラス)は親クラスがFuncExecutor型なので、上のdemoメソッド内のstateMethod_への代入は有効です。これでupdateメソッド内でタイトルのexecメソッドが呼ばれ、Opening_Message_gが引数に渡されます。ポリモーフィズム万歳ですね。
さて、引数とメソッドへのポインタをクラスにパックしただけで、updateメソッドから早々にswitch〜case文が消え、呼び出しがかなり整理されました。しかし、まだまだ不満です。メンバ変数にdemo_、title_そしてgame_と状態の数だけ変数が定義されていますし、その型となるクラスも長々と書き連ねています。これを解消する工夫をしてみます。
C テンプレート化でクラスの数が激減する!
先のクラスの数がどんどん増えるのはちょっと嫌ですが、どう最適化すべきか良く分かりません。そこで、2つのFuncExec派生クラスを比べてみます:
FuncExecutorの派生クラスの比較 // int型引数を持つメソッド
class func_Int : public FuncExecutor {
public:
void ( GameScene::*func )( int );
int arg_; // 引数
virtual void exec( GameScene *pObj ) {
( pObj->*func_ )( arg_ );
}
};
// char*型引数を持つメソッド
class func_CharPtr : public FuncExecutor {
public:
void ( GameScene::*func )( char* );
char* arg_;
virtual void exec( GameScene *pObj ) {
( pObj->*func_ )( arg_ );
}
};
一目瞭然の傾向があります。2つのクラスで違うところはメソッドの引数の型だけです。後は全部まったくぴったり同じになっています!これは「テンプレート化」の格好の形になっているんです。これをテンプレートクラスにするとこうなります:
FuncExecutorの派生テンプレートクラス(引数1) // 引数を1つ持つテンプレートFuncExec派生クラス
template< class ARG1 >
class Arg1Func : public FuncExecutor {
public:
void ( GameScene::*func_ )( ARG1 );
ARG1 arg_; // 引数
virtual void exec( GameScene *pObj ) {
( pObj->*func_ )( arg_ );
}
};
ARG1というのがステートメソッドの1番目の引数の型です。このテンプレートを使うと引数が1つのステートメソッドに対して同じテンプレートが使えます。例えばこんな感じです:
テンプレートを用いた宣言 class GameScene {
private:
Arg1Func< int > demo_; // 引数1つ
Arg1Func< char* > title_;
Arg1Func< int, GAMELEVEL > game_; // 引数2つ
};
Arg1Funcは名前の通り引数を2つ持つテンプレートクラスです。先の煩わしいクラスの型が今や引数の数のみで区別されるようになってしまいました。もちろん、クラス数は激減です。
D クラス内テンプレートから独立したテンプレートへ
上のテンプレートクラスはかなり使えます。ただ、クラス内に宣言されているので、このクラスでしか使えません。他の状態遷移クラスでは、また同様の宣言をする必要が出てきます。これはかなりうざったいわけです。
テンプレートのfuncメソッドに注目します。これ、
void ( GameScene::* )( ARG1 )
という型で宣言されています。太文字にクラス名が入っていますね。と言う事は、他の状態遷移クラスになると、ここが、
void( OtherScene::* )( ARG1 )
などとそのクラスの名前になるわけです。他の部分は全く一緒なのにも関わらずに…。もうお分かりかと思いますが、ここもテンプレート引数にしてしまえばいいんです:
FuncExecutorの派生テンプレートクラスをさらに一般化 // 親クラスも変える必要があります
template< class CLS >
class FuncExecutor {
public:
virtual void exec( CLS *pObj ) = 0;
};
// 引数を1つ持つテンプレートFuncExec派生クラス
template< class CLS, class ARG1 >
class Arg1Func : public FuncExec< CLS > {
public:
virtual void exec( CLS *pObj ) {
( pObj->*func_ )( arg_ );
}
public:
void ( CLS::*func_ )( ARG1 );
ARG1 arg_; // 引数
};
CLSというテンプレート引数を追加しました。ここには状態遷移クラスの名前が入ります。こうすることで、このテンプレートクラスはめでたくGameSceneクラスから離れ、1つの独立したテンプレートとして分離することができるようになりました!このテンプレートを使うと、GameSceneクラス内にあったメンバの宣言は次のように変わります:
一般化したFuncExecutorテンプレートを用いた宣言例 class GameScene {
private:
Arg1Func< GameScene, int > demo_; // 引数1つ
Arg1Func< GameScene, char* > title_;
Arg1Func< GameScene, int, GAMELEVEL > game_; // 引数2つ
FuncExecutor* stateMethod_;
};
かなり面白くなってきました。
E 遷移オブジェクトの渡し方にこだわりたい!
ここまででかなり一般化してきました。まだ改良の余地が無いかよ〜く考えます。するとステートメソッドの変更部分が気になりました。stateMethod_に次のステートメソッドを登録する部分をもう一度見てみます:
遷移オブジェクトの登録ですが… // デモ
void GameScene::demo( int mode ) {
title_.arg_ = TitleString_g; // 文字列へのポインタを代入
stateMethod_ = &title_; // タイトルに遷移させる
}
確かにこれでも問題は無いのですが、引数とメソッドの代入が分離しているのがちょっと嫌なんです。引数の設定をし忘れることもあるかもしれないわけです。ここを、
遷移オブジェクトの登録はこうしたい // デモ
void GameScene::demo( int mode ) {
stateMethod_ = &title_( TitleString_g ); // タイトルに遷移させる
}
とできるとなんだかメソッドを呼び出しているみたいで分かりやすいですよね。でも、title_って単なるメンバでメソッドではありません。それをこのような関数の呼び出しの形にできるのでしょうか?これが出来るんです。これを実現するには「( )」をオーバーロードします。「( )」は「関数呼び出し演算子」と呼ばれています:
テンプレートクラスの関数呼び出し演算子をオーバーロード // 引数を1つ持つテンプレートFuncExec派生クラス
template< class CLS, class ARG1 >
class Arg1Func : public FuncExec< CLS > {
public:
virtual void exec( CLS *pObj ) {
( pObj->*func_ )( arg_ );
}
Arg1Func& operator ()( ARG1 arg1 ) {
arg_ = arg1;
return *this;
}
private:
ARG1 arg_; // 引数
public:
void ( CLS::*func_ )( ARG1 );
};
関数呼び出し演算子をオーバーロードして、引数にARG1を取れるようにしました。こうすると「変数名()」という関数のような呼び出しができるようになります。しかも、この演算子の戻り値は自分自身ですから、stateMethod_に直接代入する事ができるわけです。
上のテンプレートのもう1つの改良点。関数呼び出しのような方法で引数を代入できるようになったので、arg_メンバをprivateにして保護するように変更しています。…となるとfunc_も保護したいなぁと思うわけです。もちろんsetFuncPtrメソッドのようなセッター(設定メソッド)を定義して代入しても良いのですが、せっかく関数呼び出し演算子のオーバーロードがあるのですから、もう1つ定義して引数にメソッドポインタを入れてしまいましょう:
メソッドポインタも渡していいじゃない // 引数を1つ持つテンプレートFuncExec派生クラス
template< class CLS, class ARG1 >
class Arg1Func : public FuncExec< CLS > {
public:
virtual void exec( CLS *pObj ) {
( pObj->*func_ )( arg_ );
}
Arg1Func& operator ()( ARG1 arg1 ) {
arg_ = arg1;
return *this;
}
Arg1Func& operator ()( void ( CLS::*func )( ARG1 ), ARG1 arg1 ) {
arg_ = arg1;
func_ = func;
return *this;
}
private:
ARG1 arg_; // 引数
void ( CLS::*func_ )( ARG1 );
};
これでメンバはすべて保護されました。
もうかなりのレベルまでリファクタリングが完了しています。今一度残された部分を見てみます:
残りはここだ! class GameScene {
private:
Arg1Func< GameScene, int > demo_; // 引数1つ
Arg1Func< GameScene, char* > title_;
Arg1Func< GameScene, int, GAMELEVEL > game_; // 引数2つ
FuncExecutor* stateMethod_;
};
メソッドと対をなすメンバ変数名。この2重化は未だ解決していません。これを消すのは実はかなり厄介なんです。なぜか?stateMethod_に例えばdemo_への参照を渡して状態遷移を起こすとします。これが出来るのは「demo_という実体があるから」です。もし上の宣言を無くしてしまうと、果たして実体をどこに持てば良いのでしょうか?この所在を考えるのが実はかなり大変でした。でも、解決できたんです。
F 実体が無ければ動的に持てばいいんだけど・・・
メンバに実体を持たずとも、その所在を保持する方法が1つあります。それは「動的確保」です。要はnewを使ってヒープに実体を持てば、メンバ変数に定義する必要は無くなります。例えばdemoメソッド内で次のように状態を遷移させます:
動的確保による状態遷移 // デモ
void GameScene::demo( int mode ) {
stateMethod_ = new Arg1Func( &GameScene::title_, TitleString_g );
}
ちなみに、こういうコンストラクタを作ったとしてです。確かにこうすれば、stateMethod_はタイトルメソッドを保持したクラスのオブジェクトを指してくれます。しかし、この実装はあまり良くありません。まず、別の状態に遷移する時にdeleteが発生します(つまり上の実装はまずい…)。状態遷移の度にdelete文を書くのは面倒ですし、書き損じるとリークが発生します。
ただ、そこはまだ気を付ければいいんです。上の実装で真にまずいのは、むしろステートの変更の度にnewが呼ばれる所なんです。小さなnewが高頻度で呼ばれると、メモリの断片化が加速します。そして何よりも、newとdeleteの繰り返しはパフォーマンスを犠牲にします。メモリにも速度にもやさしくない実装になっているわけです。
そこでふと考えます。「一度メモリを確保しておいて、毎回そこに上書きするようにすれば、確保も消去も必要なくなるんじゃないか」。これがグッドアイデアなんです。これを実現する取って置きの秘策が「placement new」です。
placement newは自分が指定したメモリ位置にオブジェクトを作ってくれる機能を持ったnewです。これを使うと上の実装はこう改良されます:
placement newによる最適化 // デモ
void GameScene::demo( int mode ) {
stateMethod_ = new( heap_ ) Arg1Func( &GameScene::title_, TitleString_g );
}
heap_という変数(ポインタ)に予め十分なメモリを確保しておきますと、上の実装でそこにArg1Funcオブジェクトが生成されます。どうせ状態遷移は1つのメソッドしか実行しないのですから、このheap_を別の箇所でも使い回す事ができるわけです。
これで、事実上クラスのメンバにあったあの煩わしい、demo_、title_そしてgame_というステートメソッドと同じ名前のメンバ変数は消えます!!
G 十分なメモリって…どのくらい?
これで、実は当初の目標は達しています。GameSceneクラスの宣言部はほぼ冒頭の採集形態と同じように綺麗に片付いています。ただ、まだ濁りは取れていないんです。
上のheap_。ここにはArg1Funcを保持するだけのメモリを確保する必要があります。でも、どれだけの大きさを確保したら良いのでしょうか?他のステートメソッドクラスの引数にでっかい構造体があったとしたら、その分だけのメモリの確保が必要です。最大となるメモリ量を事前に調査してプログラマが与える。これはヒューマンエラーの温床です。
もう1つ、もしこのテンプレートを他の人にも使ってもらうとして、そのドキュメントに「上のようにplacement newを使用してheap_に適切な大きさのメモリを確保して下さい」と書くわけです。そんな煩わしい事をユーザに強いるのは酷というものです。これでは、誰も使ってくれません。
そこで考えるのは「メモリの自動確保」。上のheap_を自動的に確保する仕組みを作るんです。こうなると、別のクラスを持ち出すしかありません。それが「FuncSetterクラス」です。
H 本丸FuncSetterクラスを実装する
この章も大詰めです。FuncSetterクラスは内部でArg1FuncクラスなどFuncExecutorクラス群をreplacement newを使って生成し、その時に必要なメモリの量を自動的に計算して確保してくれるヘルパークラスです。ユーザーはFuncSetter::setFuncメソッドを通してクラスのステートメソッドとその引数を渡します。setFuncメソッドの内部では引数のステートメソッドと引数から適切なFuncArg○オブジェクトを生成します。
ここで重要なのが、setFuncメソッドに渡される引数の型です。例えば次のようになるわけです:
FuncSetter::setFuncメソッドの使用例 // デモ
void GameScene::demo( int mode ) {
funcSetter_.setFunc( &GameScene::title_, TitleString_g );
}
titleの型を正しく書くと「void ( GameScene::*)(const char*)」型、TitleString_gというのは多分「const char*」型です。もしgameメソッドを同様にセットすると、
FuncSetter::setFuncメソッドの使用例 // デモ
void GameScene::demo( int mode ) {
funcSetter_.setFunc( &GameScene::title_, 2, GAMELEVEL_DIFFICULT );
}
です。gameの型は「void ( GameScene::*)(int, GAMELEVEL)」、他2つはそれぞれint型とGAMELEVEL型です。引数の型や数が全く違います!!数はまだしも、型の違いをどうやって吸収したら良いのでしょうか?これを解決する秘策が「テンプレートメソッド」です。
知らない人は知らないのですが、クラスはメソッド単位でもテンプレートを使う事ができます。例えば、
テンプレートメソッド template< class ARG1, class ARG2 >
void dummy( ARG1 arg1, ARG2 arg2 ) {
ARG1 arg1_ = arg1;
ARG2 arg2_ = arg2;
}
こういうメソッドを作る事ができます。この引数の型は、凄まじい事に呼ばれた方に化けます。これを使えば、どんな引数でも渡せるウルトラマルチなメソッドが出来るんです!setFuncメソッドはこれを使うわけです。
setFuncメソッドの実装(途中段階)はこういう感じになります:
FuncSetter::setFuncテンプレートメソッド template< class CLS >
class FuncSetter {
public:
//! 引数1のメソッド設定
template< class FUNC, class ARG1 >
FuncExecutor< CLS >* setFunc( FUNC func, ARG1 arg1 ) {
return new( heap_ ) Arg1Func< CLS, ARG1 >( func, arg1 );
}
private:
FuncExecutor< CLS >* heap_; // ヒープメモリ
};
第1引数がメソッドポインタ、第2引数がそのメソッドの持つ第1引数です。受けた値をそのままArg1Funcクラスに渡して動的生成しています。その実体はheap_の先にあります。一応メソッドは親クラスであるFuncExecutorへのポインタを返すようにしました。
これでsetFuncメソッドが呼ばれる度に新しいArg1Funcが作られ、ここが大切なのですが、前のオブジェクトは上書きされて消えてしまいます。ここで「やばいよ!」と思った方は素晴らしいの一言。そう、メモリを強制的に上書きすると、下地にあったオブジェクトは自分が消えた事を全く認識できません。つまり、消されるArg1Funcはクラスとして正しい終了の手順を踏めないんです。クラスが消える時には「デストラクタ」を呼ぶ必要がありまして、上の実装にそれを「明示的に」呼ぶ部分を書き加えます:
FuncSetter::setFuncテンプレートメソッド template< class CLS >
class FuncSetter {
public:
FuncSetter() : heap_( 0 ) {}
//! 引数1のメソッド設定
template< class FUNC, class ARG1 >
FuncExecutor< CLS >* setFunc( FUNC func, ARG1 arg1 ) {
if ( heap_ )
heap_->~FuncExecutor();
return new( heap_ ) Arg1Func< CLS, ARG1 >( func, arg1 );
}
private:
void* heap_; // ヒープメモリ
};
「デストラクタって呼べるの?」と思われるかもしれませんが、これは呼べます。コンストラクタは呼べませんが、デストラクタは明示的に呼べる1つのメソッドなんです。これで上書きしても安心です。
残されたのは、heap_の自動確保だけです。これはそれ程難しくはありません。というのも確保すべきクラスのサイズはsizeof演算子で計算できるからです。上の場合、もしheap_のサイズがArg1Funcの大きさよりも小さかったら確保し直します。そのために、heap_の確保サイズをFuncSetterのメンバに追加し、確保用のメソッドを1つ作ります:
FuncSetter::setFuncテンプレートメソッド template< class CLS >
class FuncSetter {
public:
FuncSetter() : heap_( 0 ), allocSize_( 0 ) {}
//! ステート実行
void exec( CLS* pObj ) {
if( heap_ ) heap_->exec( pObj );
}
//! 引数1のメソッド設定
template< class FUNC, class ARG1 >
FuncExecutor< CLS >* setFunc( FUNC func, ARG1 arg1 ) {
if ( heap_ )
heap_->~FuncExecutor();
ReallocMem( sizeof( Arg1Func< CLS, ARG1> ) );
return new( heap_ ) Arg1Func< CLS, ARG1 >( func, arg1 );
}
private:
//! ヒープメモリ確保
void ReallocMem( unsigned int size ) {
if ( allocSize_ < size ) {
heap_ = (FuncExecutor< CLS >*)realloc( heap_, size );
allocSize_ = size;
}
}
private:
unsigned int allocSize_; //!< 確保しているメモリサイズ
void* heap_; //!< ヒープメモリ
};
ReallocMemメソッドは引数のサイズと既に確保されているヒープのサイズを比較して、必要に応じてメモリを拡張するメソッドです。placement newをする前にこのメソッドを呼んでおけば、必要なサイズのメモリが確実に確保されます。
後はFuncSetter::setFuncメソッドをオーバーロードして引数無しなどのステートメソッドに対応するように実装すれば、引数の数の違いに対応できます。最後に、このクラスにもexecメソッドを追加して、ステートメソッドを内部で呼び出すようにすれば、このクラスは完成します。そして、この段階で冒頭の採集形態にすべてか完全に生まれ変わりました!!!お疲れ様です(^-^)/
I 後書き
状態遷移を実現する各テンプレートの完全な実装とその使用例はサンプルプログラムにあります。もちろん、このテンプレートクラスは自由にお使い下さい。
それにしても、最初のswitch〜case文の無理押し実装が、FuncSetterテンプレートクラスを使う事で大変にスマートに書き直す事ができるようになりました。プログラマは状態遷移をする時に、もうステート変数を設けたりする必要が一切無くなります。やる事はFuncSetterクラスのインスタンスをクラスに1つ持たせてupdateメソッドでexecメソッドを呼ぶだけ。後は、クラスのどのメソッドでも呼び出すことができます。これで引数に縛られない超柔軟な状態遷移を実現できるわけです。
ただ、たった1つ、上の実装には制約があります。それは状態遷移の戻り値が「void」である事。ここだけが束縛です。でも、これまでさんざん状態遷移をやってきた感触からすると、戻り値はvoidで十分です。どうしてもと言う方は、ここをboolなどにしても良いかもしれません。
兎にも角にも、この章は私も疲れました。ここまでお読みになった方が果たして何人いらっしゃるかわかりませんが、読破したみなさん、お疲れ様でした〜〜