ホーム < ゲームつくろー! < クラス構築編 < 改・COMポインタテンプレートクラス(交換サポート)

改・COMポインタテンプレートクラス(交換サポート)


 前章で2つのスマートポインタに登録されたオブジェクトポインタをそっくり取り替えるためにスマートポインタの改良を行いました。これと同様のことはCOMポインタについても言える訳でして、改良しない手はありません。

 しかし、COMポインタの参照カウンタ値は登録されたCOMインターフェイス自身が持っています。ここがスマートポインタと違うところです。そして、この違いが実装に何とも微妙な影響を及ぼします。

 細かい事は後述することにして、この章ではCOMインターフェイスを交換できるCOMポインタテンプレートクラスの実装についてじっくり見ていくことにしましょう。尚、以下はかなり込み入った内容になりますので、「どうやって実装しているのかな?」と興味のある方のみお読み下さい。どうでもいいから手っ取り早く使いたいという方は、こちらをどうぞ



@ 知らない所で増減する参照カウンタ数

 スマートポインタとCOMポインタで大きく違う点は、スマートポインタが参照カウンタを自身が持つのに対して、COMポインタは登録されたCOMインターフェイスがカウンタを持つ点です。このため、COMポインタの知らない所で他のCOMインターフェイスが勝手に参照カウンタを増減する状態が生じます。

 例えば、IDirect3DDevice9インターフェイスは生成時にはもちろん参照カウンタが1ですが、テクスチャを生成するD3DXCreateTexture関数などでテクスチャを生成すると、その参照カウント数が2になります。これは生成したテクスチャがIDirect3DDevice9インターフェイスを保持しているために起こります。

 この仕組みがあるお陰で、テクスチャからデバイスを取得する事もできるわけです。本サイトで紹介している従来までのCOMポインタは、自分が保持している分の参照カウンタを責任を持って増減させていたため、この知らない所での増減の影響はありませんでした。ところが、スワップを考慮する時に、これが微妙に影響してくるんです。



A 確保したダブルポインタをいつ消すか?

 COMポインタ間でCOMインターフェイスをそっくり入れ替えるには、スマートポインタの時に考慮した時と同様にダブルポインタを設ける必要があります:


上の例では、cpA、cpB、cpCの3つのCOMポインタが共通のポインタ値0x01(例:IDirectDTexture9*型)を保持しています。もし3つのいずれかがこのポインタ値を別の物にすると、共有するすべてのCOMポインタが別のインターフェイスを持つ事になります。これを利用してスワップを実現するわけです。

 0x01は、new演算子でヒープ領域から取られる事になりますが、3つのCOMポインタが存在する限りメモリ内にい続けなければ危険です。ダングリングポインタとなってしまいます。「なら、pTex01の参照カウンタが0になった時に消そう」。私は当初そう考えまして、0x01の存在期間をpTex01の参照カウンタではかっていました。所が、ここで@で説明した「知らない間で増減する参照カウンタ」が影響してきます。

 上の例だとテクスチャCOMポインタは3つです。彼らはコピーされる度にテクスチャの参照カウンタを増やします。この時、もしDirectXの何らかのインターフェイスがこのテクスチャの参照カウンタを1つ勝手に増加させているとすると、テクスチャの総参照カウンタ数は4になります。ここですべてのCOMポインタを消去すると、参照カウンタは3つ減ります。よって、pTex01の参照カウンタ数は「1」です。つまり、まだpTex01が生きているため、0x01が消されずに残ります。やがて、このテクスチャを使っていた最後の一人がいなくなると、テクスチャは自然と消去されます。でも、この時点でCOMポインタはすでにありませんから、0x01は誰から消される事も無く残ります。つまり、参照カウンタで0x01の寿命をはかると、メモリリークが発生しまくってしまうんです!!

 このようなカラクリから、インターフェイスポインタを格納する領域の寿命とそのインターフェイス自体の寿命はイコールにはなりません。では、0x01の寿命はどうやってはかるのか・・・。これは、インターフェイスとは別の参照カウンタをもう1つ設ける事で解決します!



B ダブル参照カウンタ方式でCOMポインタを実装する

 では、インターフェイスとCOM自身の2つの参照カウンタを操作する新しいCOMポインタを作ります。スマートポインタの時のノウハウもありますので、少しずつ掘り下げれば大丈夫です(^-^)。尚説明を簡素にするために、最初にメンバ変数を定義しておきます。

○ メンバ変数

 COMポインタは当然COMインターフェイスを保持します。これはテンプレートの型引数に渡された型となります。今その型をT型と定義しておきます。COMポインタが持つメンバ変数は3つあります:

COMポインタメンバ変数
template <class T>
class Com_ptr
{
protected:
   T **m_ppInterface;      // インターフェイスポインタへのポインタ
   ULONG *m_pRef;    // COMポインタの参照カウンタ
   static T* m_NullPtr;    // NULLポインタ
};


template <class T>
T* Com_ptr<T>::m_NullPtr = NULL;

m_ppObjはT型のダブルポインタです。これはAの説明の0x01というインターフェイスポインタへのアドレスを保持する変数で、ポインタ先は動的に確保されます。
m_pRefはCOMポインタ自体の参照カウンタで、m_ppObjを消去するタイミングを握ります。
m_NullPtrはstatic宣言された共有NULLポインタです。これはm_ppObjが特定のインターフェイスを指していない時に変わりに指されるポインタです。こうしないと、m_ppObjの先がダングリングポインタとなってしまうために設ける必要があります。

 以上のメンバ変数をこれから使いまわしていきます。


○ デフォルトコンストラクタ

 デフォルトコンストラクタはCOMポインタを生成する時に呼ばれます。通常は空っぽのCOMポインタを生成します。「空っぽ」というのを上のメンバ変数で表現する事がここでのポイントとなります:

デフォルトコンストラクタ
explicit Com_ptr(T* pInterface = NULL, BOOL add=FALSE)
{
   // インターフェイスのカウントアップ指示があれば従う
   if(pInterface && add)
      pInterface->AddRef();

   m_pRef = new ULONG(1); // COMポインタ参照カウンタを新規確保

   m_ppInterface = new T*; // ポインタ格納領域を新規確保
   if(pInterface)
      *m_ppInterface = pInterface;
   else
      *m_ppInterface = m_NullPtr;
}

引数がNULLの場合が真に空っぽです。その場合、m_pRefは1となりますが、m_ppInterfaceの指す先はNULLインターフェイスであるm_NullPtrを代入します。「何でこんな面倒な事を?」と唸りたくなりますが、これはT型ダブルポインタの指す先を保証するためなんです。
 引数のpInterfaceが有効な場合、これは「新規インターフェイス登録」と捉えます。外部からこの形式で渡される時、このCOMポインタはRelease権限を持つ事になります。しかし、コピーではないのでインターフェイスの参照カウンタは増やしません。m_pRefはこの段階でやはり1です。



○ 暗黙型変換コンストラクタ

 関数の引数や生成時の代入などで呼ばれるコピーコンストラクタの1つです。引数にはT2型という別の型が渡され、これを内部で保持します:

暗黙型変換コンストラクタ
template<class T2> Com_ptr( Com_ptr<T2>& src )
{
   m_pRef = src.GetMyRefPtr(); // COMポインタ参照カウンタ共有
   *m_pRef += 1; // COMポインタ参照カウンタをインクリメント

   m_ppInterface = (T**)src.GetPtrPtr(); // 共有
   *m_ppInterface = src.GetPtr(); // 型チェック用

   // 参照カウンタ増加
   if(*m_ppInterface)
      AddRef( *m_ppInterface );
}

相手はすでに初期化が終了しているので、少なくともm_ppInterfaceの先の存在は保証されています(m_NullPtrがここで威力を見せます)。ですから、個々では単純に相手と自分を共有するだけです。実際に共有するのは相手のm_ppInterfaceとm_pRefです。上ではインターフェイスポインタも代入していますが、これは型チェックをするためのダミーで、実際は同じポインタを代入しているに過ぎません。もし暗黙のポインタ変換ができない関係である場合、「型チェック用」とコメントした1行がコンパイラに引っかかります。最後にインターフェイスが存在するならそれをインクリメントして共有終了です。



○ 同型コピーコンストラクタ

 今度は同じ型のコピーコンストラクタです。純型の暗黙コピーはこちらが呼ばれます:

同型コピーコンストラクタ
Com_ptr(Com_ptr<T> &src)
{
   m_pRef = src.GetMyRefPtr(); // COMポインタ参照カウンタ共有
   *m_pRef += 1; // COMポインタ参照カウンタをインクリメント

   m_ppInterface = src.GetPtrPtr(); // 共有

   // 参照カウンタ増加
   if(*m_ppInterface)
      AddRef( *m_ppInterface );
}

やっている事は暗黙型変換の時とまったく同様です。ただチェック機構が必要ないのでその1行だけがありません。説明は重複しますので省略します。



○ NULLコピーコンストラクタ

 関数の引数などでNULLが代入される時に呼ばれるコンストラクタです。NULLは「#define NULL 0」と宣言されておりまして、これは列記とした整数です:

NULLコピーコンストラクタ
Com_ptr(const int nullval)
{
   m_ppInterface = new T*; // ポインタ格納領域を新規確保
   *m_ppInterface = m_NullPtr;
   m_pRef = new ULONG(1);
}

純粋に空っぽの状態で初期化すると考えますので、メンバ変数を初期化しているだけです。COMポインタ参照カウンタ数は1としておきます。コンストラクタ群はこれでおしまいです。



○ デストラクタ

 COMポインタがメモリから消えようとしている時に呼ばれるデストラクタでは、2つの仕事をします:

同型コピーコンストラクタ
virtual ~Com_ptr()
{
   // 有効なインターフェイスがあればリリースする
   if(*m_ppInterface)
      Release(*m_ppInterface);

   // COM参照カウンタ数が0になったら
   // m_ppInterfaceの先も必要なくなるので消去する
   if( --(*m_pRef) == 0 ){
      delete m_ppInterface;
      delete m_pRef;
   }
}

仕事その1は、保持しているインターフェイスの参照カウンタを減少させます。上ではCOMポインタ自身が持つReleaseメソッドを呼んでいますが(Releaseメソッド内のデバッガを働かせるためです)、面倒ならばインターフェイスが持つ同メソッドを直接呼んでも構いません。仕事その2は自身の参照カウンタのデクリメントと消去チェックです。これはスマートポインタとまったく同じですね。参照カウンタが0になったら、確保していたm_ppInterfaceとm_pRefをメモリから消去します。



○ =同型代入演算子

 ここからしばらくは演算子のお話です。まずは=代入演算子です。これは生成後に「=」で同型のCOMポインタを代入しようとした時に呼ばれます:

=同型代入演算子
Com_ptr<T>& operator =(Com_ptr<T>& src)
{
  // 同じCOMポインタ参照グループである場合は何もしない
   if( m_pRef == src.GetMyRefPtr() )
      return *this;

   // 自分の持つインターフェイスの参照カウンタを1つ減らす
   if(*m_ppInterface)
      Release(*m_ppInterface);

   // 自分は他人になってしまうので参照カウンタをデクリメント
   ReleaseComRef();

   // 相手をコピー
   m_ppInterface = src.m_ppInterface;
   m_pRef = src.m_pRef;

   // カウンタをインクリメントして共有
   if(*m_ppInterface)
      AddRef( *m_ppInterface );
   *m_pRef += 1;

   return *this;
}


=演算子はコンストラクタと違って自分もすでに初期化がされている状態にあるため、少し注意が必要になります。引数の相手が自分と同じ参照グループ(COMポインタ参照カウンタへのポインタが同じ)であれば、自分が相手とまったく同じインターフェイスを持っている事になるため、何もせずに終了させます。相手が異なるものであれば、自分の持つインターフェイスの参照カウンタを1つ減らし、また自身の参照カウンタもデクリメント(ReleaseComRefメソッドを新設しています)して、過去のインターフェイスと決別します。後は相手をコピーして新しいインターフェイス参照カウンタと自身の参照カウンタを増やして終わりです。



○ =暗黙型変換代入演算子

 続いて右辺が別の型である場合の代入を定義します:

=暗黙型変換代入演算子
template<class T2> Com_ptr<T>& operator =(Com_ptr<T2>& src)
{
   // 同じCOMポインタ参照グループである場合は何もしない
   if( m_pRef == src.GetMyRefPtr() )
      return *this;

   // 自分の持つインターフェイスの参照カウンタを1つ減らす
   if(*m_ppInterface)
      Release(*m_ppInterface);

   // 自分は他人になってしまうので自身の参照カウンタをデクリメント
   ReleaseComRef();

   // 相手のポインタをコピー
   m_ppInterface = (T**)src.GetPtrPtr();
   *m_ppInterface = src.GetPtr(); // チェック用代入
   m_pRef = src.GetMyRefPtr();

   // カウンタをインクリメントして共有
   if(*m_ppInterface)
   AddRef( *m_ppInterface );
      *m_pRef += 1;

   return *this;
}

基本は同型の時と同じです。相手のポインタをコピーする時にT2**型からT**型への明示的な型変換を必要とするのと、暗黙のポインタキャストをしている点が異なります。他は一緒です。



○ =NULL代入演算子

 作成後のCOMポインタにNULLを代入すると、自身の持つインターフェイスの所有権を放棄して空っぽになります。この演算子はその仕事を担います:

=NULL代入演算子
Com_ptr<T>& operator =(const int nullval)
{
   // 自分の持つインターフェイスの参照カウンタを1つ減らす
   if(*m_ppInterface)
      Release(*m_ppInterface);

   // 自分は空っぽになってしまうので自身の参照カウンタをデクリメント
   ReleaseComRef();

   // ポインタを初期化
   m_ppInterface = new T*;
   *m_ppInterface = m_NullPtr;
   m_pRef = new ULONG(1);

   return *this;
}

NULLが代入されるのですから問答無用でインターフェイスの参照カウンタを減らします。また自身の参照カウンタも減らして完全に所有権を放棄します。次に各ポインタをすべて再初期化します。



○ =新規インターフェイス代入演算子

 作成後のCOMポインタに新規のインターフェイスを代入する時に呼ばれる演算子です。1つのインターフェイスを2つのCOMポインタに新規登録すると、メモリ保護が起きるので、ちょっと気をつけなくてはいけない演算子なのですが、便利でもあるため採用しました:

=新規インターフェイス代入演算子
template<class T2> void operator =(T2* pInterface)
{
   // 明示的にインターフェイスを新規登録する場合に用いる

   // 自分の持つインターフェイスの参照カウンタを1つ減らす
   if(*m_ppInterface)
      Release(*m_ppInterface);

   // 自分は他人になってしまうので自身の参照カウンタをデクリメント
   ReleaseComRef();

   // 自分は新しい人になるので、
   // 新しいダブルポインタを作成
   m_ppInterface = new T*;
   m_pRef = new ULONG(1);

   // 新規代入
   if(pInterface)
      *m_ppInterface = pInterface;
   else
      *m_ppInterface = m_NullPtr;
}

 これは暗黙キャスト代入も視野に入れています。


○ ->メンバ選択演算子

 メンバ選択演算子は一緒ですね:

->メンバ選択演算子
T* operator ->(){ return *m_ppInterface; }

保持しているインターフェイスポインタを返すだけです。ただ、ここでもm_NullPtrが威力を発揮しているのがわかると思います。




C メンバメソッド(ToCreatorメソッド、Swapメソッド)

 メンバメソッドは細かい物が幾つかありますが、その中で重要なToCreatorメソッドとSwapメソッドについて説明します。特にこの章の目的であるSwapメソッドは詳しく見ていきます。


○ ToCreatorメソッド

 このメソッドはインターフェイスを作成する他の関数に渡す専用のメソッドです。DirectXではすべてのインターフェイスは生成関数を持っています(newできないため)。このメソッドは代入されるべきインターフェイスのダブルポインタを直接渡す働きをします:

ToCreatorメソッド
T** ToCreator()
{
   // 自分のインターフェイスの参照カウンタを1つ減らす
   if(*m_ppInterface)
      Release(*m_ppInterface);

   // 自分は他人になってしまうので自身の参照カウンタをデクリメント
   ReleaseComRef();

   // ポインタを初期化
   m_ppInterface = new T*;
   *m_ppInterface = m_NullPtr; // 一応代入しておきます
   m_pRef = new ULONG(1);

   return m_ppInterface;
}

生成メソッドに渡すということは、自分自身が別のインターフェイスを持つと言う事なので、再初期化のプロセスを先にしておけば良い事になります。言ってみればそれだけなんです(^-^;。



○ Swapメソッド

 この章の目的であるインターフェイスの交換は、ここまでの仕込みをしてようやく可能になります。ははは大変(笑)。インターフェイスを交換する時に問題になるのが「参照カウント数」でしょう。例えば2つのテクスチャpTex_AとpTex_Bがあったとして、一方は100の参照があり、もう一方は50の参照が成されているとします。テクスチャ同士ですから交換は可能ですが、そのまま交換するとCOMポインタの数が狂うために、メモリリークとメモリ保護違反がいっぺんに起こります。

 どうするべきか、下の図をご覧下さい:

 スワップする前、pTex_Aは100人、pTex_Bは50人に参照されているとしましょう。その内pTex_Aは黄色で示した35人がDirectXの内部で参照されているとします。つまりCOMポインタが関与しない参照カウンタ数です。pTex_Bについても同様です。これをスワップして下の状態にするのが正解です。

 双方のテクスチャでCOMポインタが関与しない分は絶対に保証しなければなりません。一方同じインターフェイスを共有していたCOMポインタは、その人数分だけそっくり入れ替わります。結果、上の場合pTex_Aは55人、pTex_Bは95人と参照カウントを変更すれば正しくなります。何だかクイズかパズルのような話ですね。

 「COMポインタの共有数」と言うと、ここまで散々出てきました「COMポインタ参照カウント数」がそれに該当するわけです。そこで、まず双方のm_pRefが指す参照カウント数を引き算してみます。すると、上の例ですと「45」が出てくるはずです。それをスワップ前の参照カウント数に増減してみると・・・そう、下の数字になるんですね(pTex_A = 100 - 45 = 55; pTex_B = 50 + 45 = 95)。

 この辺りを踏まえて、Swapメソッドを実装するとこうなります:

Swapメソッド
bool Swap( Com_ptr<T> &src )
{
   // 引数のCOMポインタが保持するインターフェイスと自身のとを入れ替える

   // 双方のCOMポインタ参照カウンタ数のチェック
   ULONG SrcComRef = *src.GetMyRefPtr();
   ULONG MyComRef = *m_pRef;

   // 双方のCOMポインタ参照カウンタ数の差を算出
   bool SrcDecriment = false; // 引数の参照カウンタを減少させる場合trueになる
   ULONG DefComRef = MyComRef - SrcComRef;
   if( SrcComRef > MyComRef )
   {
      // 引数の参照数の方が多いのでSrcDecrimentをtrueに
      DefComRef = SrcComRef - MyComRef;
      SrcDecriment = true;
   }

   // 参照カウンタの増加側と減少側を確定
   T *pReleaseObj, *pAddObj;
   if(SrcDecriment){
      pReleaseObj = src.GetPtr(); // 引数のを減少
      pAddObj = *m_ppInterface;
   }
   else{
      pReleaseObj = *m_ppInterface;
      pAddObj = src.GetPtr(); // 引数のを増加
   }

   // 互いの参照カウント数を交換
   ULONG i;
   if(pReleaseObj && pAddObj) // 双方が有効なインターフェイス
   {
      for(i=0; i<DefComRef; i++){
         pReleaseObj->Release();
         pAddObj->AddRef();
      }
   }
   else if(pReleaseObj && (pAddObj==NULL)) // 減少側だけが有効
   {
      for(i=0; i<DefComRef; i++)
         pReleaseObj->Release();
   }
   else if((pReleaseObj==NULL) && pAddObj) // 増加側だけが有効
   {
      for(i=0; i<DefComRef; i++)
         pAddObj->AddRef();
   }

   // COMポインタ内のインターフェイスポインタを交換
   T* pTmp = *src.m_ppInterface;
   *src.m_ppInterface = *m_ppInterface;
   *m_ppInterface = pTmp;

   return true;
}


 やけに長いのは引数のインターフェイスポインタもしくは自分がNULLである場合を考慮しているためです。先ほど説明した事が実装されています。



 大変な章でしたが、これでスマートポインタと同様にCOMポインタもスワップができるようになりました。沢山いるモブキャラクタのテクスチャを一度に取り替えたり、コンテナに納められているインターフェイスを取り替えるなど、スワップが使える場面と言うのは結構あります。この章で機能追加したCOMポインタクラスは公開いたしますので、ご自由にお使い下さい。