ホーム < ゲームつくろー! < クラス構築編 < 改改・スマートポインタテンプレートクラス(交換サポート)
改改・スマートポインタテンプレートクラス(交換サポート)
クラス構築編で作成したスマートポインタは、自分が保持するポインタとその参照カウンタポインタをコピーすることで自動削除を実現しています。ところが、この実装をしばらく使っていて「あ、これスワップ出来ない」事に気がつきました。
現在の使用は以下のような図式になっています:
1つのスマートポイント内には2つのポインタがあります。1つはT型のオブジェクトへのポインタ(オレンジ)、もう1つは参照カウンタへのポインタ(緑)です。上図では2つのグループがあることがわかります。
今SP03とSP04をスワップさせようと頑張ってみます:
しかし、変数をどういじくっても互いが持つポインタを入れ替えているだけになります。これは単なる代入のし合いです。今やりたいのはグループ全体が指すオブジェクトを入れ替えたいのです(図右側)。しかし、参照カウンタの整数値はポインタを通せば可能ですが、オブジェクトの「実体」を入れ替えるのは大変難しいことなんです。それはポインタ型から指すオブジェクトのサイズを算出できないためです。つまり、今の仕様ではスワップは不可能です。
そこで、次のように考えてみます:
オレンジが指すのを実体ではなくて青色で示したポインタにしてしまうんです。そして青色のポインタが実体を指すようにします。こうすると、青いポインタを入れ替える事でグループ全体が指す実体がすべて入れ替わります。もちろんこれまでの使い方は変わりません。この仕様変更は他のクラスに対して完全に透明です。
この仕様変更を施したスマートポインタ(v2.00)をこちらで公開致します。とりあえず使ってみたい方はどうぞ。以下はプログラムの中身のお話なので、どう実装しているか、また自分なりに改変してみたい方はお読みください。
@ T**型のメンバ変数を動的に生成
ここから先はコードに興味のある方だけどうぞ。現在の仕様ではスマートポインタテンプレートクラスはT*型のメンバ変数を持っています。冒頭で示しましたように、これをハードコピーしているためにグループのスワップが出来ないのでした。
そこで、これをすぐ上の図のようにT**型に変更し、これをハードコピーしていきます。こうするとグループ内の誰かがポインタの先を変更すると、グループ全体の指す実体が入れ替わります:
クラスのメンバ変数宣言はこんな感じです:
spクラスメンバ変数宣言 template <class T>
class sp
{
private:
unsigned int *m_pRefCnt; // 参照カウンタへのポインタ
T** m_ppPtr; // T型オブジェクトへのダブルポインタ
static T* m_NullPtr; // NULLポインタ値
};
m_ppPtrが指す先(青い四角)は存在が保証されないといけません。よってこれはnewによって動的に確保します。また太文字でしめしたstatic宣言のT型ポインタ変数は、青い四角が指す先がNULLである時に代入される仮ポインタです。これが無いと実はえらい大変なんです。これら変数の確保タイミングの1つはコンストラクタです:
sp::spコンストラクタ explicit sp(T* src=NULL, int add=0)
{
m_pRefCnt = new UINT;
*m_pRefCnt = add;
m_ppPtr = new T*; // T型のポインタ変数の格納場所を動的に作成
m_NullPtr = NULL;
if(src)
*m_ppPtr = src;
else
*m_ppPtr = m_NullPtr; // NULLポインタを入れておく
AddRef(); // 参照カウンタ増加
}
引数のsrcもnewされているはず(スマートポインタの使い方の基本)なので、srcが有効ならばこの段階で3つのヒープメモリが確保されていることになります。それらすべての削除をスマートポインタが担います。
削除のタイミングは参照カウンタが0になった時で、これはReleaseメソッドが担います:
sp::Releaseメソッド void Release()
{
if(--(*m_pRefCnt) == 0){
delete *m_ppPtr;
delete m_ppPtr;
delete m_pRefCnt;
}
}
「m_NullPtrが入っている時にdelete *m_ppPtrなんてして大丈夫?」と思われるかもしれませんが、NULLへのdeleteは認められています。もちろん何の作用もしないだけです。
A コピーコンストラクタ
コピーコンストラクタは同型のスマートポインタを=演算子以外で代入する時の振る舞いを定義します。今回の改変スマートポインタの場合、相手からコピーするのはT**型の値と参照カウンタポインタになります。T**型の値をコピーするとポインタが指す先が相手と共有されます:
コピーした後に参照カウンタを1つ増やします。忘れると大変です(^-^;。コードはこんな感じになります:
sp::spコピーコンストラクタ sp(const sp<T> &src)
{
// 相手のポインタをすべてコピー
m_pRefCnt = src.m_pRefCnt; // 参照カウンタポインタ
m_ppPtr = src.m_ppPtr; // T型ダブルポインタ
// 参照カウンタを増加
AddRef();
}
B 暗黙アップキャストコピー
コピーのもう1つは暗黙のアップキャストコピーです。これは=演算子を用いずにアップキャストをする時に呼ばれます。関数の引数とか、宣言時初期化などですね。コピーコンストラクタと違い、これはちょっと注意が必要です。まずは図をご覧ください:
親クラスのポインタに子クラスのポインタを代入することは可能です(多態性)。しかし、ダブルポインタにはそういう概念はありません。「じゃだめなの?」と思ってしまいますが、そんなことありません。実は、単純にポインタとして明示的にキャストコピーしちゃえばいいです。これは「どのようなポインタのサイズも処理系内では必ず同じ」というC言語の保証があるために可能です:
sp::sp暗黙アップキャストコピー template<class T2> sp(sp<T2> &src)
{
// 相手のダブルポインタを明示キャストしてコピー
m_pRefCnt = src.GetRefPtr();
m_ppPtr = (T**)src.GetPtrPtr();
// 型チェックコピー
*m_ppPtr = src.GetPtr(); // ここでコンパイルチェックを入れられる
// 自分自身の参照カウンタを増加
AddRef();
}
「いやいや!だってint**型にObjectClass**型を入れるのは危険だろ!」と思われるかもしれません。もちろんそういうキャストは危険です。そこで、上のプログラムには1つ細工がしてあります。「型チェックコピー」というところで、相手のT2型ポインタを自分のT型ポインタに流し込んでいます。もし、ここにコンパイルエラーが出るならば、暗黙キャストできない代入をしようとしています。一方コンパイルが通るならば、親に子を入れるなどの適正な暗黙キャストがされていることになります。つまり、この代入はチェック機構になっているわけです。
C =代入演算子のオーバーロード
今度は明示代入である=演算子です。ここはソースを見た方が早いかもしれません:
sp::operator =代入演算子 sp<T>& operator =(const sp<T> &src)
{
// 自分自身への代入は不正で意味が無いので
// 行わない。
if(*src.m_ppPtr == *m_ppPtr)
return (*this);
// 自分は他人になってしまうので
// 参照カウンタを1つ減少
Release();
// 相手のポインタをコピー
m_pRefCnt = src.m_pRefCnt;
m_ppPtr = src.m_ppPtr;
// 新しい自分自身の参照カウンタを増加
AddRef();
return (*this);
}
大切なのは太文字で示した部分です。=代入演算子の場合、すでに生成されている自分自身が右辺に来る状態がありえます。太文字部分は自分と同じグループが右辺にきた場合、その代入処理をスキップします。もしこの処理が無いと、
sp<int> spInt( new int );
spInt = spInt = spInt = spInt;
という代入作業をする度に参照カウンタが増えてしまいます。spIntがスコープアウトした時も参照カウンタは3ですから、メモリリークが発生してしまいます。上の2行は極めて大切なんです。
D NULL代入
スマートポインタをリセットする時にはNULLを代入します。これによりそのスマートポインタはグループから脱却して無所属になります。グループから抜けるということは、グループ内の参照カウンタ数が1つ減るわけです。また保持しているポインタ変数はすべて新しくnewされます。NULLというのは実体ですからm_ppPtrではなくて*m_pPtrにm_NullPtrが代入されることになります:
sp::operator =代入演算子(NULL代入) sp<T>& operator =(const int nullval)
{
// 自分は空な人になってしまうので
// 参照カウンタを1つ減少
Release();
// 新規に自分自身を作る
m_pRefCnt = new unsigned int(1);
m_ppPtr = new T*;
m_*ppPtr = m_NullPtr;
return (*this);
}
太文字部分をしてからNULLポインタ代入です。
E 間接参照演算子(*)・メンバ選択演算子(->)
これら演算子はスマートポインタが通常のポインタであるかのように振舞うために必要です。特に->演算子は重要ですね。*間接参照演算子は要は保持している実体へのアドレスを返すようにすれば良いので、
sp::operator *間接参照演算子 T& operator *()
{
return **m_ppPtr;
}
とします。実はここでNULLポインタを設けた威力が発揮されます。m_ppPtrをNULLにするとこの演算子が指す先でメモリ保護違反が生じてしまうんです。備えあればなんとやらです。
メンバ選択演算子は次の通り:
sp::operator ->メンバ選択演算子 T* operator ->()
{
return *m_ppPtr;
}
F ダウンキャストコピー
動的型判定サポートによるダウンキャストコピーもメンバメソッドによってサポートします。これはdynamic_cast演算子によってポインタ変換をすれば良いだけですが、慎重に実装します:
sp::DownCastメソッド template <class T2> bool DownCast(sp<T2> &src)
{
// 引数のスマートポインタの持つポインタが、
// 自分の登録しているポインタに
// ダウンキャスト可能な場合はダウンキャストコピーを実行
T* castPtr = dynamic_cast<T*>(src.GetPtr());
if(castPtr){
// ダウンキャスト成功
// 自分は違う人になるので
// 既存の参照カウンタを1つ減少
Release();
// 新しいポインタと参照カウンタを共有
m_ppPtr = (T**)src.GetPtrPtr(); // dynamic_castでもうチェック済み
*m_ppPtr = castPtr;
m_pRefCnt = src.GetRefPtr();
// 参照カウンタ増加
AddRef();
return true;
}
return false;
}
G スワップ
本章の目的であったスワップは、ここまでの変更で非常に簡単に行えます:
sp::Swapメソッド void SwapPtr( sp<T> &src )
{
T* pTmp = src.GetPtr();
*src.m_ppPtr = *m_ppPtr; // ポインタの交換
*m_ppPtr = pTmp;
}
いわゆるポインタの交換をしているだけですが、これを実現するために、いやぁ大変でした(T_T)。
H 改定スマートポインタ公開
本章で作成しましたスマートポインタをこちらより公開致します。使い方は極めて簡単で且つ非常に便利なものなので、どうぞご使用ください。