第七章.シーン管理クラスの設計1 2000/10/26
C#のことを少し書こうかと思っていたのだが、スーパープログラマへの道で少し書いたあと、やはり実際に触ってみないことにはよくわからないという結論に達し、やはり書かないことにする(笑)
そんなわけで今回は、ゲームのシーン管理の方法について書く。
ゲームは、それぞれをシーン管理すべきである。シーンとは、メーカーロゴ、タイトル画面、オープニング、ゲーム画面、ゲームオーバー画面、ハイスコア画面というような構成単位である。ゲームの種類によっては、ゲーム画面にコンティニュー画面とゲームオーバー画面が含まれたりするかも知れないし、オープニング画面にハイスコア画面が含まれたりもするかも知れない。
yaneSDK1stのほうには、シーン管理の手段まで提供していたのだが、個人的にはあまりいいとは思わなかったのでyaneSDK2ndになったときにさくっと消した。確か、EL(easy link library)のほうも、シーン管理の手段を提供してあったように思うが、あれも同じく美しくはない。
かと言って、このようなシーンの実装をC++でどうやっていいのかわからない人が多いし、よく相談も受ける。それほど難しいことではないので、ここでは私のやりかたを紹介する。
まず、シーン名を用意しよう。
enum SCENE { NULL_SCENE,SCENE1,SCENE2,SCENE3,EXIT_SCENE }; |
NULL_SCENEは、無効なシーン。あとで使うだけで、いまは気にしなくて良い。とりあえず、こいつのFactoryを作る。
class CSceneFactory { public: static CScene*CreateScene(引数は考え中^^); }; |
とりあえず、引数には、上でenumしたSCENEを取って、そいつに応じて、シーンクラスをnewして返せばいいだろう。CSceneクラスは、シーンクラスの基底となるわけだ。ついでに、こいつの管理クラスを用意しよう。名前は、CSeneControlがいいな。どうやって使うかというと、yaneSDK2ndのメインスレッドからならば、
void CApp::MainThread(void) {
// これが実行される GetDraw()->SetDisplay(false); // Windowモード CFPSTimer t; t.SetFPS(30); CSceneControl sc; sc.SetNextScene(SCENE1); while(IsThreadValid()) { // これがValidの間、まわり続ける sc.OnDraw(GetDraw()); if (sc.GetScene()==NULL) break; // シーン終わってるやん.. GetDraw()->OnDraw(); t.WaitFrame(); } } |
というように、SetNextSceneでスタートシーンを設定して、そのあとは、描画ループでCSceneControl::OnDrawを呼び出し続ける。CSceneControlは、現在のシーン(CSceneのポインタ)を保持していて、そのOnDrawに委譲する。シーンクラスは、次のシーンに移動したくなったら、CScene::SetNextSceneで次のシーンに移動できる…といいなぁ(笑)
それじゃ、シーンコントロールクラスは、こんな感じ?
class CSceneControl { public: CSceneControl(void) { m_eNextScene = NULL_SCENE; } void SetNextScene(SCENE eScene){ m_eNextScene = eScene; } void OnDraw(CDIBDraw*lpDraw){ if (m_eNextScene!=NULL_SCENE){ m_lpScene.Add(CSceneFactory::CreateScene(m_eNextScene,this)); m_eNextScene = NULL_SCENE; } if (m_lpScene!=NULL) { m_lpScene->OnDraw(lpDraw); } } private: SCENE m_eNextScene; auto_ptrEx<CScene> m_lpScene; }; |
auto_ptrExの実装は、yaneSDK2ndのauto_ptrExを見てください。(簡単に説明すると、上のリストにあるAddは、所有権を発生させるオブジェクト移動なので、前に指していたオブジェクトが所有権付きだとそれを解放してから代入します) ここでやっている処理は、SetNextSceneでは、シーンの解放は行なわないで、CSceneControl::OnDrawのなかで、次のシーンが設定されていれば前のシーンを解放し、そして次のシーンをnewして代入するということです。もしシーンに初期化を伴うのであれば、Addのあとm_lpScene->Initializeを呼び出すのも良し。また、このAddをする瞬間に一時的に、2つのシーン(のインスタンス)両方がメモリ上に存在することになるので、場合によっては、メモリを一時的にですが圧迫することになります。その場合、前のシーンの解放を終了してから次のシーンをnewしたほうが良いです。
それはともかく、シーンクラスは、こんな感じですか?
class CScene { public: CScene(CSceneControl*lp) { m_lpSceneControl=lp; } virtual void OnDraw(CDIBDraw*) { } virtual ~CScene( ) { } // place holder protected: void SetNextScene(SCENE eScene){ m_lpSceneControl->SetNextScene(eScene); } private: CSceneControl* m_lpSceneControl; }; |
SetNextSceneを行なえるのは、CSceneControlなので、そちらに委譲します。そちらに委譲するために、コンストラクタでCSceneControlのポインタをもらっておいたのでした。そんなわけで、さきほどのCSceneFactoryは、こんな感じ?
CScene* CSceneFactory::CreateScene(SCENE
e,CSceneControl*lp){ switch (e) { case SCENE1 : return new CScene1(lp); case SCENE2 : return new CScene2(lp); case SCENE3 : return new CScene3(lp); default : return NULL; } } |
実際のシーンは、CSceneから派生させてOnDrawをオーバーライドして使う。次のシーンに行きたくなったら、SetNextSceneを呼び出せば、次の描画タイミングで次のシーンに移行できる…と。まあ、シーン管理、やりかたはいろいろありそうですが、ひとつの方法ということで。
一応、動作テストに書いたゴミのようなプログラムがあるので、yaneSDK2ndを使っている人は煮るなり焼くなりしてください。(笑)
第八章.シーン管理クラスの設計2 2000/11/05
いま、めっちゃ忙しいのですが、ちょっと息抜きにシーン管理について書きます。
シーン管理には、さまざまな方法があります。たとえばA,B,Cというシーンがあって、このシーンをA,B,A,B,Cの順番で呼び出す必要があるとすれば、それはどのように実現すれば良いのでしょうか?
一見すると、有限状態オートマトンの問題に還元できそうですが、実は、この手のシーン管理をそのようにするのは具の骨頂で、前回Bというシーンだったから、次はAというシーンに飛びなさい、というようなプログラムにしてはいかんのです。なぜなら、A,B,A,B,Cと呼び出さなければならないときに、現在、1回目のBなのか2回目のBなのかという情報をどこかに保持しなければいけないからです。
そんなことなら、最初から、シーンを連番で管理すれば良いと思うかも知れません。つまり、1=A,2=B,3=A,4=B,5=Cというように、番号付けして、シーンがinvalidate(≒終了)したら、この番号によって次のシーンに移行するようなシーン管理クラスを作ってしまえば良いと言われるかも知れません。そこまでは、とても簡単なことです。
ところが、ある一連のシーンをひとまとめにして流したい場合、それらにいちいち連番を振らなければならないのでしょうか?たとえば、お祭りイベントは、とうもろこしイベント,林檎飴イベント,とうもろこしイベント,林檎飴イベント,金魚すくいイベントの順番でシーンを呼び出すことによって実現するとします。さらに、とうもろこしイベントは、A,B,Cの3つのシーンを順番に呼び出すことによって構成されるとします。こういうようにイベントが一種の階層構造を形成していることはよくあるのです。
その階層構造をいちいち展開して連番で管理しないといけないとしたら、それまた大変なことなのです。
実は、階層構造(的呼び出し)は、スタックひとつあれば、それで十分なので、
enum eSCENE { SC_1,SC_2,SC_3,SC_4,...,SC_30
}; // シーン(階層構造であることもありうる)の列挙
stack<eSCENE> m_eSceneStack;
というようなクラスメンバを用意して、このような
SC_1 := SC_2 + SC_3 + SC_4 // シーン1とは、シーン2,シーン3,シーン4の呼び出しで構成されている
SC_2 := SC_10 + SC_12 // シーン2とは、シーン10,シーン12の呼び出しで構成されている
階層構造を持ったシーンを実現したいならば、イメージとしては、以下のような感じになるでしょう。
void CSceneAdmin::SetNextScene(eSCENE eS
/* = SC_NULL */ ) { eSCENE eScene; if ( eS!=SC_NULL) { eScene = eS; } else { eScene = m_eSceneStack.pop( ); } switch ( eScene){ case SC_1: m_eSceneStack.push(SC_4); // スタックなので呼び出したいシーンを逆順で積む m_eSceneStack.push(SC_3); // スタックなので呼び出したいシーンを逆順で積む m_eSceneStack.push(SC_2); // スタックなので呼び出したいシーンを逆順で積む SetNextScene( ); // 再帰的呼び出しによって解決 break; case SC_2: m_eSceneStack.push(SC_12); // スタックなので呼び出したいシーンを逆順で積む m_eSceneStack.push(SC_10); // スタックなので呼び出したいシーンを逆順で積む SetNextScene( ); // 再帰的呼び出しによって解決 break; case SC_3: CSceneControl::SetNextScene(es); break; (中略) } } |
まあ、具体的な内容は、前回のシーン管理クラスも参考にしていただくとして、ここで言いたいのは、このような階層構造(的呼び出し)は、(擬似)スタックが、ひとつあれば実装可能な論理構造だということです。BASICのサブルーチンを形成するgosub〜returnがいかにネストされていようと、スタック一本で実現されるのと同じです。
シーン管理における有限状態オートマトンの破棄に始まり、連番によるシーン管理から、シーン擬似スタックを導入しての階層化シーン呼び出しに成功しました。裏を返せば、有限状態オートマトンは階層構造を表現するのに適していないため、このようなシーン管理を実現しにくいのです。
yaneSDK1stにあったようなシーン管理、そしてel(easy link library)にあるようなシーン管理はダサいから破棄してしまったほうがいいと私が思ったのは、そのためです。
第九章.コピーコンストラクタは必要か? 2000/11/22
急遽、別ラインで作っていたはずの『HAPPY ほたる荘』のプログラムを私がやることになったので、えらい迷惑してます。そんなわけで、しばらくホームページはお休みさせていただくかも知れません。とか言いつつ、こんなの書いててえんかな〜とか思わなくもないですが^^;
今回は、コピーコンストラクタの話です。
ポインタを含むクラスオブジェクトを正しくコピーするには、コピーコンストラクタを用意するかと思います。しかし、それは、本当に必要なことなのか?とか最近思うわけです。
たとえば、ビットマップを扱うCDIBクラスがあったとして、これのコピーが本当に必要なケースなんてどんな場合だろう?と思うのです。画像エフェクトをかけるためにまずビットマップの複製を作りたい、ということはあるかも知れません。しかし、それはかなりレアケースであって、仮にコピーが必要だったとしてもコピーコンストラクタで行なうべき処理ではないような気がするのです。
あるオブジェクトの配列を作りたいとします。単純に考えると、std::vector<CDIB>とやりそうです。
しかし、std::vectorは、配列サイズの自動拡張を行なうときに、要素のコピーを必要とします。すなわち、operator = で、適切にコピーできないクラスに対しては正しく動作しないということです。ならば、コピーコンストラクタを作ればいいかというと、そういう結論にはならないと思うんです。
なぜなら、CDIBクラスのコピーは、明らかに某大な計算コストを伴います。リソースをstd::stringクラスのように共有するためのスマートポインタのような仕組みを導入すれば良いのでしょうか?これは従来のプログラミングスタイルでは一つの解答かも知れませんが、CDIBに関しては、それもやはり違うような気がします。複製が欲しいときは、明示的に複製したほうがすっきりします。
そうなってくると、operator = は、protectedにして、クラスを使うユーザーが operator = でのコピーをしようとすると、コンパイルエラーが出るようにしたのち、vector<auto_ptr<CDIB > >と、ポインタのvectorを採用するべきでしょう。
しかし、そうなってくると、ポインタなわけで、アロー演算子( -> )でアクセスしなければならないという馬鹿げた事態に陥るわけです。
「アロー演算子を使うことの何が馬鹿げてるんだよ!」
と言われるかも知れません。やねうらおは、実際、ベテランプログラマの人に、そう言われました。きっと、そのベテランプログラマの方は、ポインタ演算なんて当たり前だし、慣れっこになっているんでしょう。しかし、私に言わせれば、ポインタと、実インスタンスを扱う構文が異なるということ、これはとても馬鹿げています。どちらも、メンバアクセス演算子なのだから、同じ表現であるほうが便利です。
たとえば、引数に、int &xなどと参照を使って書けるのは、とても素晴らしいギミックです。ポインタ演算を透過的に扱えるようになるからです。呼び出し元の変数をいじくりたいな、と思ったら、引数リストに & をつけるだけでOKです。もし、この機構が無かったらどれだけ悲惨な事態になるのか想像に難くはありません。
ということで、言いたいことは見えてきました。 vector<auto_ptr<CDIB > >からいかにしてアロー演算子を消すかです。
第10章.それでも継承が必要か? 2001/02/03
前回の最後の問題ですが、まあ、可搬性のある方法はC++の範囲では無理なんではないかなーという結論に至り、とりあえず、もっと強烈なテンプレート機能を自分で実装しよう!というわかったようなわからんような決心をするに至ったのです。いずれ、その話も書きたいと思っているのですが、まだ準備中なので、今回は、継承の必要性について考えるわけです。
C++は、差分プログラムが主流だと思われています。他人のクラスを自分用に拡張(もとのクラスに含まれるbugを取り除く意味も含まれる)したければ、すべからく派生によって解決します。
ただし、このように派生によってクラスを拡張するためには、クラスがある程度の条件を満たしている必要があります。つまり、virtualなデストラクタが用意されていること、オーバーライドしようとする関数がvirtualであることは当然として、関数が機能単位で綺麗にまとまっていなくてはなりません。たとえば、オーバーライドしようにも何百行もある関数をオーバーライドして、それに代わる記述をしようと思うと、差分だけで済まなくなります。
このへんの条件を運良く満たしたとしても、次のような事態に陥ることはあります。
やねうらおが作ったAというクラスの拡張のためにこいつから派生させて、AExというクラスを用意した。ところが、やねうらおは、AというクラスとBというクラスをメンバとして持ったCというクラスを設計していた。このクラスCのAをAExに差し替えたいのだが、それはやねうらおのライブラリなので、ままならない。結局、自分はやねうらおのソースを見ながら、AExとBというクラスをメンバとして持ったCExというクラスを作らなくてはならないと。こうなってくるともう泥沼で、何が何だかわからなくなってきます。
これの解決策はいろいろありますし、いずれ突っ込んで考えたいと思っていますが、このように、継承は潜在的に問題を抱えているわけです。
継承の抱えるもう一つの問題は、多重継承にまつわるものです。多重継承は必要か?と言えば、やねうらおには、よくわかりません。実際、C++の多重継承は便利だと思うものの、誰かの作ったAというクラスと、自分のクラスBとから多重継承したCというクラスを用意していたのですが、Cというクラスで新しく用意していた関数と同じ名前の関数をAというクラスにある日突然追加されて、(それをオーバーライドしていることになって)正常に動かなくなったということがありました。そういう意味では、C#のようにオーバーライドは明示的に行なえばいいと言う話もありますが、Object Pascalのように多重継承が無くても、どうにかなることはなります。ただ、委譲するだけの関数をつらつらと書くのは苦痛です。そういう意味では、それを緩和してくれるだけのテンプレート機構があればいいのですが…と、冒頭のところに話が戻ってしまうのです。
そんなことを言っていても、埒があかないので、とりあえず継承を避ける手段を考えます。一つは、メンバ関数コールバックを実現しなくてはなりません。順を追って説明します。
たとえば、ノベル系のゲームで、背景を描画したあとにユーザー定義の描画関数を呼びたいとします。単純に考えると、その関数はvirtualにしておいて、派生クラス側でオーバーライドしてもらえれば良いように思うかも知れません。しかし、こうすると、前述のような問題を引きおこし兼ねないのです。それを避けて、クラスの派生を使わないとすれば、それは関数ポインタを引き渡し、コールバックする仕組みを作るしかありません。何せ、C++は、関数閉包(関数を持ち運びするための機構)をサポートしていないのですから。
ところが、C++で、あるクラスの非staticなメンバ関数にコールバックしようとすると、thisポインタが必要になってきます。このthisポインタとセットで関数ポインタを渡しておかなければなりません。おまけに、メンバ関数ポインタのサイズは、一般に固定ではありません。(スーパープログラマへの道 第94回を参照のこと) よって、これを汎用型にキャストするようなことは出来ないのです。このような汎用型が無いから、無駄にテンプレートを持ち出さなくてはならなくなるのが、C++の悪いところです。
とりあえず、C++でならテンプレートで解決してみましょう。
class CFunctionCallbacker { public: virtual LRESULT Call(DWORD dwParam) { return 0; } virtual ~CFunctionCallbacker(){} }; template <class T> class CTFunctionCallbacker : public CFunctionCallbacker { public: CTFunctionCallbacker(void){ m_pThis = NULL; m_pFunc = NULL; } virtual LRESULT Call(DWORD dwParam) { if (m_pThis) return (m_pThis->*m_pFunc)(dwParam); return 0; } virtual void SetFunction(T* pThis,LRESULT (T::*pFunc)(DWORD)){ m_pThis = pThis; m_pFunc = pFunc; } private: T* m_pThis; LRESULT (T::*m_pFunc)(DWORD); }; |
CApp::CBKFuncにコールバックをかけたければ、
CTFunctionCallbacker<CApp> tFc;
tFc->SetFunction(this,CBKFunc);
のように設定して、tFcをCFunctionCallbackerにアップキャストして使えばいいわけです。
実際のところ、こういうのは確保〜解放の仕組みが煩雑なので、auto_ptrと絡めて使います。そのへんについては次回詳しく書きます。
ひとまず、これで、C++風なコールバック(メンバ関数コールバック)を実現するための下地は出来ました。メンバ関数コールバックの引数がDWORD固定ってのが、非常にダサい気もしますが、C++のテンプレートは型に対してのみのテンプレートで、可変引数に対するテンプレートではないので仕方ない意味はあります。(これもまた、C++のテンプレートの不満でもあります)
ところで、このメンバ関数コールバックの適用例ですが、たとえば、あるメッセージ(例:Windowメッセージ)をdispatch(配信)するための仕組みとして、このメンバ関数コールバックが使えるでしょうか?
ひとまず、使えます。また、std::mapに、メッセージ名(実際はenumかdefineされた整数値)と、それに対するコールバック先を関連付けておけば、この手のdispatchは、外部のコントロールクラス(例:yaneSDK2ndにあるCWinHookのようなもの)を使わずに解決できます。ただ、dispatchするメッセージの種類によっては、ある値からある値までの範囲にあるものすべてに対して処理したい場合、それらすべてをstd::mapに一つずつ登録しておかなくてはならなくなるので、この値の範囲が大きいと、結構、メモリの無駄になる場合もあります。std::map自体、それほど効率がいいわけでもないので、std::mapで実装するかどうかは、ケースバイケースというところでしょう。
また、ひとつのメッセージに対して複数のコールバックを許す場合はstd::multi_mapを使えます。Windowメッセージの場合、こちらのほうが向いています。ただし、複数のコールバック関数を登録する場合、コールバック順序には細心の注意が必要で、コールバックの優先順位を付けるような実装も考えられます。
ともかく、メンバ関数コールバックを使えば、多重継承は少し減ります。通常の継承もある程度、減るかも知れません。次回は、auto_ptrと絡めて使う方法を説明します。
第11章.オブジェクトコンポジションを活用する 2001/02/13
継承を避けようとすると、あるクラスは、他のクラス(のインスタンス)をメンバとして持つことが多くなります。今回は、そのコンポジットのあり方について考えたいと思います。
class XYZ { class X x; class Y y; class Z z; }; |
この場合、クラスXYZは、クラスX,Y,Zから成ります。オブジェクトを扱う上で、注意しなくてはならないのは大切なのは、そのライフタイムです。生成はともかく、解体責任については常に明確にしておかなければなりません。この例では、クラスX,Y,Zのライフタイムは、クラスXYZと同じと考えていいでしょう。つまりは、クラスX,Y,Zの解体責任は、クラスXYZにあるわけです。一昔前ならば、これはhas-a関係と呼ばれていたかも知れません。しかし、誰がそのオブジェクトを所有しているかはさほど重要ではありません。(よって、この用語は現在ではほとんど使われていません)
たとえばクラスXYZは、その実装において、クラスX,Y,Zをそれぞれ使いたいだけで、そのライフタイムが必ずしも同じである必要がない場合があります。たとえば、キー入力デバイス(クラスのインスタンス)は、アプリケーションで1つあれば十分です。ならば、
class XYZ { class X* pX; class Y* pY; class Z* pZ; }; |
のようにポインタで宣言し、外部からこのポインタを設定してやれば良いと思われるかも知れません。これは、ある意味正しいです。人によっては、has-a関係とこういうのをひとまとめにして「集約関係」と呼ぶかも知れません。結局、ライフタイムの違いだけです。
よって、問題となってくるのは、その解体責任です。外部からポインタを設定すると、必ずこの問題に遭遇します。
安全を期すには、auto_ptrを使うことです。auto_ptr<X> pX;というようにauto_ptrで宣言し、
void XYZ::SetX(auto_ptr<X> p) { pX
= p; }
void XYZ::SetX(X* p) { pX = p; }
と二つのポインタ設定関数を定義することです。もし、解体責任をクラスXYZに押し付けたければ
auto_ptr<X> pX(new X); // 所有権付きオブジェクトを作成
xyz.SetX(pX); // 所有権を譲渡する
というようにすればいいでしょうし、解体責任を外部クラス側で受け持つのであれば、
xyz.SetX(GetOuterX( ) );
として、XYZ::SetX(X*)を呼び出せば良いでしょう。ここで、注意すべきことは、auto_ptrのコンストラクタはexplicitで宣言されているので、X*からauto_ptr<X>へは暗黙で変換できないということです。そのため、上のようにSetXが2種類必要になります。これは少し不便です。auto_ptrのコンストラクタがexplicitでないバージョンを用意してもいいのですが、auto_ptrはコンストラクタで引数を指定されると所有権付きオブジェクトを生成するため、暗黙の変換によって余計な問題を引き起こし兼ねません。
そこで、
1.ディフォルトでは所有権を持たない
2.必要があれば、コンストラクタの第二引数で
trueを渡せば所有権付きオブジェクトの作成される
ようなauto_ptrを用意すればいいのです。auto_ptrだなんてケチなことを言わずに、スマートポインタを実装してみましょう。
ソースが少し長くなりましたが、これです。所有権を持つ場合は、そのオブジェクトを共有しているオブジェクトに対して双方向のチェインを持っており(双方向リスト)、smart_ptr間のコピーにおいてはこのチェインをいじります。デストラクトされるときに、自分しかそのオブジェクトを所有しているsmart_ptrが無ければ、そのオブジェクトをdeleteします。
どこからともなく突っ込みが入ると嫌なので言っておきますが、名著『More Effective C++』の第28項に類似の記事がありますが、そこには、完全な実装が載っておらず、かつ、双方向リストで実装するというのは私のアイデアです。まあ、思いつく人は思いつくでしょうから、オリジナルを主張する気はありません。
それから、同じく『More Effective C++』の29項目にある参照回数計測のテクニックを使えば、双方向リストではなく、参照回数だけを保持することで済みます。そういう実装の仕方も有力ですが、その場合、通常のポインタアクセスがポインタ間接ポインタになるのと、newが余分に一回増えるのが少し嫌かなと思いました。あと、自分と同一のオブジェクトを指している他のsmart_ptrがわからないので、そのオブジェクトを強制解放することが出来ないのです。まあ、それは出来なくても問題は無いでしょうが…。
ともかく、このsmart_ptrを使えば、
void XYZ::SetX(smart_ptr<X> p) { pX = p; }
で済みます。また、使用するほうは、
xyz.SetX(smart_ptr<X>(new X,true)); // 所有権付きオブジェクトを作ってそれを渡す場合
のようにすればOKです。あと、このsmart_ptrの配列を管理するバージョンsmart_arrayも用意しました。(auto_ptrに対するauto_arrayのようなもの)
余談になりますが、smart_arrayで配列参照(operator [ ])の返し値をsmart_ptrを返すような実装にして、要素アクセスには必ずそれを使うようにすれば、メモリ管理に関してはかなり安全な機構が得られると思うのですが、どうも遅そうなのと、ポリモーフィックな配列は不可(スーパープログラマへの道 第C9回と『More Effective C++』3項目を参照のこと)なので、あまりありがたくもないかなと思い、実装しませんでした。
ポリモーフィックな配列については少し未練があるので書いておきますが、smart_arrayでも、operator = に、メンバ関数テンプレートを用いて
template <class S>
smart_array<T>& operator=(const
S* _P) {
Delete();
m_nSize = sizeof (S); // クラスSの要素サイズを記憶しておく
m_lpa = const_cast<S*>(_P); // これにはS→Tへのアップキャストが含まれることがある
return (*this);
}
のようにして行ない、operator [ ]では、必ずm_nSizeから要素位置を計算するような実装にすれば、ある程度うまく動きます。ただ、ここで与えられるS*が、S派生クラスのポインタをアップキャストしたものでない保証が得られないので、いまひとつです。smart_arrayは、一応、そういう実装にはしてあります。注意して使えば、ポリモーフィックな配列が使えます。それなりに便利です。
もう一つ。operator [ ]で、インデックスの範囲をチェックして、配列外ならば例外をthrow(assertでもいいが)するような実装も考えられます。これまた非常に面白いと思うのですが、問題が2つあります。一つは、ポインタを渡されたときにその配列サイズを取得する手段が無いことです。
これを避けるために、生成は必ずsmart_arrayに管理させたいのですが、生成に引数を伴う場合(コンストラクタに引数が必要な場合)はそうもいきません。引数をそのままtemplate化出来ると良いのですが…。
仮に、ポインタからsmart_arrayへの代入のときには、配列サイズまで指定するような実装にすれば、この問題は解決しますが(このサンプルのsmart_arrayはそういう実装にしてあります)、配列の要素アクセスのために、常にインデックス付きでアクセスしなければならないのはどうなんでしょうか…。このへんがダサイので、
smart_array<int>::iterator
のように、iteratorまで実装して、そのiteratorのなかで配列の範囲チェックまで行なえば良いわけですが、smart_arrayのほうに新たな代入等が起こったときに、iteratorにそれを通知すべきか??などと考えると泥沼に入りそうです。(iteratorには通知しなくても良いような気がします。このサンプルもそうしてあります) ただ、iteratorも、さきほどの双方向リストに参加させれば、smart_arrayが解体されてもiteratorが生きていれば実オブジェクトを解放しないような実装にすることは出来ます。そうなっているほうが正しいと思うのですが、STLのようにsmart_array::end( )と比較しながらiteratorで巡回するときに、テンポラリオブジェクトがどうも重たそうなのでそういう実装にはしませんでした。まあ、いまどきのマシンであれば、どうということは無いでしょうが…。
それともう一つ。smart_ptrならば、std::vector < smart_ptr < T > >が正常にresize等が行なえます。なるほど、smartです。
まあ、これでオブジェクトコンポジションでクラス編成を行なうための基礎基盤が出来ました。前回のメンバ関数コールバック等も、このsmart_ptr(別にauto_ptrでも良いが)を使えば、かなりすっきりします。
しかし、メンバ関数コールバックのところでも書いたとおり、C++のテンプレートでは引数が固定であることから逃れられないのです。引数固定のコールバックであれば、それこそ、特定のメッセージ(Windowsメッセージとか)のコールバックにしか使えません。
引数を可変するために、引数にクラステンプレートを取ることは出来るでしょう。(STLのmem_funとかそれっぽいです) それ以上の汎化されたコールバックが行ないたいのであれば、インターフェースクラス(意味がわからない人は関数がvirtualで宣言されているクラスと読みかえてください)を渡して、そいつを呼び出したほうが手っ取り早いでしょう。
もちろん、インターフェースクラスは、ここで出てきたsmart_ptrを使うほうが良いのは言うまでもありません。
class CButtonEventListener {
public:
virtual void OnRBClick(void){ }// 右ボタンクリック
virtual void OnLBClick(void){ } // 左ボタンクリック
};
class CButton {
public:
void SetEvent(smart_ptr<CButtonEventListener>
pv) { m_pvButtonEvent = pv; }
// 実際の使用は、毎フレームこれを呼び出す
LRESULT OnDraw( );
CButton() {
m_pvButtonEvent.Add(); // default handler
}
protected:
smart_ptr<CButtonEventListener> m_pvButtonEvent;
};
CButtonにCButtonEventListener派生クラスのsmart_ptrを渡して、CButtonからは、コールバック( m_pvButtonEvent->OnRBCLick ( );だとか)して使います。
これで、汎化されたパラメータをとる関数に対してコールバックする仕組みは出来ました。
ひとつの問題は、「CButtonEventListenerから派生して使わなければならない」という部分の「派生」、というところでしょうか。派生は確かに悪ですが、インターフェースクラスに対する実装と考えれば、それほど悪くはありません。真に問題なのは、この派生クラス側から、呼び出し側クラス内のクラスメンバにアクセス出来ないことなのです。
最初に、メンバ関数コールバックが必要だった(必要になった)理由は、そこにあります。クラスメンバにアクセスしたいのです。
たとえば、クラスCAppのなかで、上のCButtonを使うとします。そのCButtonの御用聞き(コールバック用インターフェースクラス)として、CButtonEventListenerを用意したのは良かったのですが、そのなかからCAppのクラスメンバにアクセス出来ないのでは、コールバックとは呼べないでしょう。ガキの使いにも近いものがあります。
Javaの内部クラス(クラス内クラス)では、その外にあるクラスのクラスメンバにアクセスできます。これと同じ機構を実現するには、CButtonEventListenerクラスに、CApp*を渡しておけばいいわけです。
template <class T> class mediator { public: mediator(T* pT=NULL) : m_pT(pT) {} void SetOutClass(T*pT) { m_pT = pT;} T& GetOutClass() { return *m_pT;} private: T* m_pT; }; |
このようなmediatorを作ってこの御用聞きは、mediator<CApp>とCButtonEventListenerクラスとから多重継承して派生させて、使えばいいわけです。このような、ひとつのクラスと複数の(ひとつ以上の)インターフェースクラスからの多重継承(Javaのextendsとimplementsキーワードの組み合わせ)は、かなり安全な種類の多重継承だと言えます。
あとは、マクロを介して、
#define outer GetOutClass()
outer.m_x = 10;
というように、CAppのクラスメンバに対してouterキーワードよって直接的にアクセス出来そうです。面白いのは、外部のメンバなので少し遅いように思うかも知れませんが、実際のところは、クラスメンバはthis間接でアクセスしているわけで、上のように書いても、それがインライン展開されれば、m_pT間接アクセスとなって、処理速度は(ほぼ)同じなのです。
ということで、このJavaの内部クラスの概念をさらに敷衍させて、オブジェクト指向スクリプトを自作するのならば、あるオブジェクトがあるクラス内で生成されたときに、outerキーワードによってその外部クラスにアクセスできればどうかと思ったのですよ。動的に呼び出される(名前解決がね)から遅そうに思われるかも知れませんが、実は動的にやる必要はありません。オブジェクトはどこで生成されているかはコンパイル段階に確定しているのですから、outerキーワードが出てきたときに、メンバ関数テンプレートのコンパイル処理のようにして解決出来るのです。(実行ファイルサイズは大きくなるかも知れませんが) スーパープログラマへの道 第BB回の最後で「生成されたリージョン」と書いているのは、そういう意味です。
ただ、この静的に解決する仕組みは、多重にouterを使う(outer.outer.m_xのように)ときなどまで考慮すると相当実装するのが面倒くさそうで、かつ、そんなに効果も得られない気もするので(笑)、おそらくはJavaの内部クラスのような実装(結局は上のmediator)になることでしょう。
読者の方で、もっといいアイデアをお持ちの方がおられれば、教えてください。
次回は、オブジェクト間のメッセージsendingとライフタイムの管理について詳しく書きます。
第11章.フォローアップ 2001/02/25
実装については、ここで提示したものはあくまでサンプルなどで、バグ等が含まれているかも知れません。流用されるならば、yaneSDK2ndのYTLフォルダのものを使ったほうが確実でしょう。
new / deleteの実装については、スーパープログラマへの道 「第D7回 new / deleteは遅いのか?」も参考のこと。
第12章.ポインタは安全か 2001/02/26
今回は、時間が無いので、走り書きです。ちょっと前回の配列に対する要素をどう扱うかについて書いておきたいことがあるので、それを書きます。オブジェクトを安全に扱うにはポインタを廃してスマートポインタ(smart_ptr)を導入するというところまでは、異論ないと思います。
このとき、やはり「ポインタが使えない」ということを嘆く人がいます。C++のパワーユーザーで、Javaへの批判として、比較的多いのが、これだと思います。
それに対して「ポインタを廃したのだから、ポインタが使えないのは当り前」だとか、「ポインタなんてものはJavaの設計理念に反する」だとか、「ポインタみたいに危ないものは使えないほうがいい」だとか、「速度と安全性とのトレードオフである」だとか、そういう反論をする人も多数見受けられます。それで、まあ、そういう意見にある程度納得する人もいるかとは思います。
ところが、私はこれらの意見は、すべて間違っていると思うのです。この前者の人がなぜポインタが使えないことを嘆いているのか、その理由を説明しましょう。
まずC++的なポインタとは、どういう概念なのでしょうか?主に「オブジェクトをポイントすることが出来ること」だと思われていると思うのですが、違うと思うのです。たとえば、配列の途中の要素、
T a [ 10];の
& a [ 5 ]
を、ポインタとして渡すことがあります。このとき、ポインタが無ければ、配列と、そのインデックスつまり、a と 5と2つを保持しなくてはいけません。(余談ですが、このaを保持するためには、T*だけでは不可で、本当はsizeof(T)も保持しておかないと、このポインタのインクリメント演算を正しく実装できません) 非常に不便です。
そして、ポインタの2つ目の機能として、配列の要素でないもの(非配列)も、ポインタとみなせます。
& obj
この二つを統括的に扱えるのがポインタの大きな特長です。
前者においては、算術演算が出来ます。つまり、T *p = & a[5]; であれば、これをインクリメントすればpは a[6]を指します。これも非常に便利なのです。たとえば、Javaにおいては文字列はstringとして渡すか、インデクサを渡さなければなりません。これの途中の文字列を指すポインタ(C言語のchar *のようなもの)が使えないからです。char *のようなポインタ概念を投入すると、メモリバイオレーションを引き起こしかねないからです。
果たして、本当にそうでしょうか?
iterator(たとえばstd::vector::iterator)を考えてみてください。擬似的な算術演算(配列に対する巡回演算)をサポートしながらも、* によるメモリ参照のときにvector::begin 〜 vector::endの範囲内であるかどうかをチェックすることは出来ます。(std::vector::iteratorはそういう実装にはなっていませんが)
つまり、ポインタではなく、iteratorを使えばいいのです。JavaやC#でオブジェクトをprimitive data type( int や byteのような基本型)以外は参照で扱う(つまりは内部的にはsmart_ptrで実装する)ようにした発想と同様に、ポインタ表現は(内部的には)iteratorで実装すれば良いわけです。そうすれば、完全に安全なオブジェクト操作機構が得られます。
では、iteratorを使えば、ポインタの役割は完全に果たすでしょうか?
まだ足りませんね。先の例での、 & obj のように非配列オブジェクトのアドレスは、iteratorの概念には包括されないからです。
そこで、& T からT::vector::iteratorへの変換を定義してやるのです。どうやって実装するかはいま問題ではありません。概念的なものとして考えてください。このとき、もはや、iteratorとは呼べないかも知れません。仮にここでは“ポインタ的iterator”と呼ぶことにします。
これによって、完全に安全なポインタ演算を獲得することが出来ました。ポインタ的な演算をサポートしながらも、セーフでかつ、内部的には T* と 配列へのインデクサで管理されている(ような実装にすると思う)ので、範囲内であるかどうかをチェックするのに必要な処理は、インデクサをunsignedとみなして配列の要素数と一回比較するだけです。これならば非常に高速です。
いいことづくめに思える、このポインタ的iteratorですが、まだやることは残っています。それは、これがポインタの拡張概念であるのだから、std::vector::iterator と std::list::iterator 等とコンパチでなければなりません。もう何をやるかわかったはずです。
そうです。ポインタ的iterator基底クラスを作り、std::vector::iteratorもstd::list::iteratorも、そこから派生させます。
こうしておけば、すべてポインタ的iteratorひとつで統合的に扱えるわけです。ここまでして、初めて通常のポインタ機能を備えたと言えるでしょう。つまり、安全なポインタの実装は可能。演算コストもわずか。しかも、コレクションに対するiteratorまですべてポインタとしてポインタ的に扱えるというわけです。( C++のiteratorがどうしてそうなっていないのかは、やねうらおの知るところではありません)
これで、冒頭で書いたJavaでポインタが使えないから不便というのは、正しい発想で、かつ、それに対する反論はほとんどが間違っているという理由もわかっていただけたと思います。
私の開発中(予定^^;)のオブジェクト指向スクリプトには、このようなポインタを実装しようと思っています。
第12章.フォローアップ 2001/03/03
ポインタ的iteratorで、ひとつ解決しなければならない問題があって、それは、iteratorがスマートiteratorでなければならないということです。つまり、std::vectorのように、resizeが起こったときにメモリの再確保をすることもあるような実装になっていたりすると、そこを指しているiteratorは無効になるからです。(スーパープログラマへの道 第C7回参照のこと)
そこで、メモリの再確保等を行なったときにiteratorにそれを通知してやらなければなりません。実装はそれほど難しくは無いのですが、iteratorを大量に放し飼い(笑)にしていると、すべてに通知するのは大変な時間がかかります。
そこで、通知しないと割り切る実装も当然考えられるのですが(STLのiteratorはそうなってます)それでは安全なメモリアクセスは保証されなくなってくるわけです。
第13章.シーン管理クラスの設計3 2000/03/04
シーン管理クラスの話を引っ張りますが(笑)、mediatorの話が終わってからでないと、この話は書けないと思って保留していました。走り書きですが、結構質問が多いので書きます。
そんなわけで、第7章・第8章で説明したシーンクラスをきちんとした実装をお見せするわけですが、シーンクラスの中から当然、呼び出し元のクラス(CAppクラス)にアクセスしたいことがあるわけで、ここにmediatorを導入します。結局、呼び出し元クラスにアクセスするためには、その呼び出し元クラスへのポインタをどこかで渡してやらなければならないという至極当たり前の原則に基づくものです。
問題は、それを誰がいつ渡すか、ということに尽きるでしょう。CSceneを管理しているのはCSceneControlクラスなので、こいつがCAppへのポインタをついでに設定してやれば良いと思うかも知れません。しかし、それでは、CSceneControlがCAppポインタを仲介しなければならないようになるので、CSceneControlはCAppクラスと癒着します。
ここで、void*にいったんキャストしてしまうことも可能ですが、あとでCApp*にアップキャストするのは見た目も綺麗なものではありません。テンプレートで対応できなくもないですが、そもそも、CSceneControlクラスは、派生させて使うクラスではないのでこれは避けたいのです。
よく考えてみれば、CSceneFactoryは、必ず派生させて使うクラスなので、この派生クラスをmediatorと多重継承して、そのなかで、CAppポインタをCSceneクラスに設定してやればいいということになるでしょう。SceneFactoryとCAppが癒着するのは、少しおかしい気もしますが、CSceneControlと癒着するよりいくぶん健全でしょう。
まあ、ぐだぐだ言ってても仕方ないので、CScene.hとCScene.cppをお見せします。std::stackを使って、シーン管理を実装しています。シーンクラスは、自分を解放して次のシーンにJump/Callすることもできますし、自分を解放せずに次のシーンをCallすることも出来ます。
ひとつ難しい点があるとすれば、ユーザー側のシーン名の定義としてenumを使いますが、CSceneもCSceneFactoryは、このenumについて知っていなければなりません。これを解決するために、CScene、CSceneFactory、CSceneControlの3つをテンプレート化することは考えられますが、こんなenumのためだけにテンプレート化するのは馬鹿げています。そもそも、CSceneControlは派生させずに使いたいクラスなのです。
ここではメンバ関数テンプレートを使って解決します。
// enumを無理やりintにキャストするためにメンバ関数テンプレートを用いる^^; template <class T> void JumpScene(T eScene){ JumpScene( (int)eScene ); } virtual void JumpScene(int nScene){ m_nNextScene = nScene; m_nMessage=1; } |
少しダサい気もしますが、これで我慢しておきましょう(笑) というか、enumしたものはintに暗黙で変換されるので、C++では、こんなことをしなくてもいいという話はあります。そのわりにはswitch文では(int)にキャストしないといけないです。よくわかりません。
もう一つの不満は、CPlaneBase*という描画コンテキスト(描画サーフェイス)を意味するポインタがこのクラスのなかに入り込んできていることでしょう。void*にいったんキャストして、その後、dynamic_castするような設計もアリでしょうが、上のようなメンバ関数テンプレートだけでは解決できない(と思います。)
メンバ関数テンプレートはvirtualには出来ないのでオーバーライドして使う関数CScene::OnDrawに対してメンバ関数テンプレートは使えないのです。
これを愚痴っても仕方ないのかも知れませんが、テンプレートのように型ごとに関数を展開する機能とは別に、型隠蔽をする機能が欲しくなります。C#のboxing〜unboxingがそれに近いのかも知れませんが、あれは結局void*にキャストしたのちにdynamic_castしているのと大差ない気がします。とは言っても、型隠蔽するのにそういうアプローチ以外は考えにくいので、やはりメンバ関数テンプレートを拡張するのが正しいような気もします。
あと、第8章で書いたシーン管理クラスも、実は、このCSceneから派生させ、シーン管理クラス内のOnDrawは実際は描画せずにCallSceneで他のシーンを次々と呼び出すタイプの実装にすれば、第7章のシーン管理クラスと第8章のシーン管理クラスが統合できます。イメージとしては、ノベル系のゲームなら、こんな感じになります。
図を途中までを丁寧に書いて、途中からとっても落書き〜な感じになる
あたりが私の性格を如実に反映してますね^^;
また、サブシーンを持つ場合、すなわち、ゲーム中の画面を表示しながらハイスコア表示をしたいような場合、ハイスコアのシーンクラスのなかで、ゲーム中の画面クラスを生成し、そちらに委譲するといい場合もありますし、さらにシーンクラスのなかで、あらたにCSceneControlを用意して、サブシーンを管理するような場合もあります。また、4人同時プレーで画面を4分割するならば、シーンクラス内で、4つこのCSceneControlを生成してシーン管理をすると便利でしょう。
まあ組み合わせを書いていてはキリが無いので、今回はこのへんにて。シーン管理クラスについては、まだ書かなければならないことがあるので、いずれ別の形で書きます。