スマートポインタテンプレートクラス
クラスの中である型へのポインタを持ち、外部で動的に生成したオブジェクトを渡すと言う事は良くあります。例えば以下のような感じです。
class CEnemy
{
protected:
CShip *m_pShip; // 船へのポインタ
public:
// 船オブジェクトのポインタを設定
void SetShipPtr(CShip *ship);
}
このクラスのSetShipPtr関数を用いて外部で動的に生成したShipへのポインタを保持します。これにより、敵(Enemy)はターゲットとなる宇宙船の位置等を把握できるようになるわけです。
ところで、こうして得たポインタの先に実体がある事を保障できるでしょうか?実体が変わるのはまだ良いのですが、実体がない場合にはポインタを通したアクセスは不正となります。下手をするとメモリ保護違反でアプリケーションが止まってしまいます。
スマートポインタは参照カウンタ方式を取る事によりこの「ポインタの先にある実体」を保証します。参照カウンタ方式についてはDirectXクラス構築編「インターフェイスのコピー問題と解放作業を解決するCom_ptrクラス」を参照してください。
@ smart_ptrテンプレートクラスの構築
smart_ptrテンプレートクラスは、new演算子で動的に確保されたポインタ変数を受け取り、その管理と削除を行います。内部の参照カウンタでそのポインタ先の実体の寿命を管理します。自分自身がスコープから外れる時に、デストラクタで参照カウンタを減らし、もしカウンタが0だったら責任を持ってその変数をdelete演算子で削除します。
Com_ptrクラスではインターフェイスが参照カウンタを持っていましたが、smart_ptrクラスは自分自身が参照カウンタを持ちます。本サイトのスマートポインタでは、「スマートポインタオブジェクトが生成された時点で、ポインタの保持不保持に関わらず参照カウンタは1」とします。こうすると、ポインタの先に実体が存在しない場合が生じます。しかし、スマートポインタを通す限り「実体がない場合はNULL」と言うのが保証されます。ポインタアクセス時にNULLかどうかをチェックすれば、少なくとも解放された後の不定の実体へアクセスする事は無くなります。
メンバ変数は、参照カウンタへのポインタであるm_pRefCntとテンプレートの型Tへのポインタであるm_pPtrです。m_pRefCnt先の実体はsmart_ptrオブジェクトが作成される時に作成されます。デフォルトコンストラクタは次のようになります。
explicit smart_ptr(T* src=NULL)
{
// ポインタを保持
m_pPtr = src;
// 参照カウンタ変数を作成
m_pRefCnt = new UINT;
*m_pRefCnt = 0;
AddRef();
}
引数には外部でnew演算子を用いて作成したT型のオブジェクトへのポインタが渡されます。それを内部で保持し作成した参照カウンタを増加させます。この時点でこのポインタの先の削除権限がsmart_ptrに移譲されます。explicit宣言されているので暗黙の型変換は出来ませんが、親クラスの型で定義した物に対して派生クラスのオブジェクトへのポインタを渡す事はできます。
AddRef関数は参照カウンタを1つ増やす関数です。参照カウンタを増減させる関数としてAddRef関数及びRelease関数を定義します。
void smart_ptr::AddRef(){ (*m_pRefCnt)++;}
void smart_ptr::Release()
{
// 参照カウンタを減少
(*m_pRefCnt)--;
// もし参照カウンタが0だったらポインタ先の実体と
// 参照カウンタ変数を削除
if(m_pRefCnt==0){
delete[] m_pPtr;
delete m_pRefCnt;
}
}
参照カウンタの増加は簡単ですが、カウンタの減少は少し注意があります。それは削除する時には必ずdelete[]を用いると言う事です。これは保持しているポインタが「配列として確保されている」かもしれないからです。通常の動的なメモリ確保は要素数1の配列と扱われるものでもdelete[]で削除する事が出来るますが、delete演算子では配列を削除でません。
(2008. 12. 14追記)
↑のdelete演算子の挙動は処理系依存があるようです。確かに昔VC++6などで同様の事をしたらエラーになった記憶があります。現在のマルペケスマートポインタは代入時に配列か否かをプログラマが指定するようにしています。
(了)
暗黙的なコピーで使用されるコピーコンストラクタは次のようになります。
smart_ptr(const smart_ptr<T> &src)
{
// ポインタコピー
m_pRefCnt = src.m_pRefCnt;
m_pPtr = sec.n_pPtr;
// 参照カウンタを増加
AddRef();
}
}
暗黙的なコピーがされる場合、まずコピー元の情報を全てコピーしてから、自分自身のAddRef関数を呼び出します。コピーの前にsrc.AddRef();としてからコピーしても結果は同じなのですがこれは出来ません。というのは、srcがconst宣言で呼ばれているため、srcが保持するAddRef関数が呼べないのです(*thisポインタを渡せない)。
明示的なコピーで使用される=演算子は次のようになります。
smart_ptr& operator =(const smart_ptr<T> &src)
{
// 自分自身へのコピーは意味が無いので
// 行わない
if(src.m_pPtr = m_pPtr)
return (*this);
// 自分の参照カウンタを1つ減少
Release();
// ポインタコピー
m_pRefCnt = sec.m_pRefCnt;
m_pPtr = sec.n_pPtr;
// 参照カウンタを増加
src.AddRef();
return (*this);
}
=演算子による演算は、オブジェクトを生成した後に行われるため、すでのポインタを保持している可能性があります。そのため、コピー時にはまず自分の参照カウンタを減らしRelease処理を終わらせ、コピーしたら自分自身のAddRef関数で参照カウンタを1つ増やす必要があります。ただし、自分自身へのコピーを行う事は意味が無いので何もしません。これは実は重要で、最初のif文が無いと、次のRelease関数で解放されてしまったオブジェクトを指すポインタ(ダングリングポインタ)をコピーする事になってしまいます。
smart_ptrを生成した後にポインタを移譲する場合もあります。そのためにSetPtr関数を設けます。この時、すでにsmart_ptrにポインタが入っている場合があるので、Release処理後に再初期化を行います。
void SetPtr(T* src = NULL){
// 参照カウンタを減らした後に再初期化
Release();
m_pRefCnt = new UINT;
*m_pRefCnt = 0;
m_pPtr = src;
AddRef();
}
保持しているポインタはGetPtr関数で明示的に取得するとします。GetPtr関数を使う場合、それは「ポインタを貸す」作業と考え、参照カウンタの増減は行いません。
T* GetPtr(){return m_pPtr);
最後にデストラクタでは、Release関数により参照カウンタを1つ減らすだけです。コンストラクタでm_pRefCntを動的に生成していますが、その削除についてももう考える必要はありません。どこか別のsmart_ptr内で参照カウンタが0になった時、正しく削除されます。
virtual ~smart_ptr(){
// 参照カウンタを減少
Release();
}
以上でsmart_ptrクラスの実装ができました。完全実装はこちらになります。
A smart_ptrテンプレートクラスの使用例
smart_ptrクラスの例のために、最初に示したCEnemyクラスを使ってみます。
CEnemyクラスの内部にはCShipポインタをじかに持つのではなく、スマートポインタとして保持します。
class CEnemy
{
protected:
smart_ptr<CShip> m_spShip; // 船へのスマートポインタ
public:
// 船オブジェクトのポインタを設定
void SetShipPtr(smart_ptr<CShip> ship);
}
こうすると、CEnemyクラスはsmart_ptr<CShip>という形でなければCShipオブジェクトを受け付けません。コンストラクタでexplicit宣言がされているので、
CEnemy En;
CShip *pShip = NULL;
En.SetShipPtr(pShip);
とSetShipPtr関数にポインタをダイレクトに入れる事は出来なくなります。
CShipオブジェクトは動的に作成しますが、次のように生成すると間違いが無くなります。
smart_ptr<CShip> spShip(new CShip);
つまり、スマートポインタのデフォルトコンストラクタに一気に代入してしまうわけです。もちろん、
smart_ptr<CShip> spShip;
CShip *p = new CShip;
spShip.SetPtr(p);
のように生成してからSetPtr関数で代入してもかまいませんが、デフォルトコンストラクタを用いると間違いがありません。new演算子で作成した後の解放はsmart_ptrが行いますので、プログラマは解放作業を気する必要はありません。
smart_ptrの強みはコピーの時に発揮されます。CEnemyオブジェクトが2つある時、
CEnemy E1, E2;
smart_ptr<CShip> spShip(new CShip);
E1 = E2 = spShip;
というコピーを行うと、spShipの参照カウンタは「3」となり、これら全てのオブジェクトが無くならない限り、new CShipで確保したオブジェクトの存在が保証されます。これは、スコープをまたぐので、大変使い勝手がよいのです。
B スマートポインタの応用
スマートポインタの応用先としてはデザインパターンの1つであるファクトリーパターンが考えられます。これは、ある親クラス及びその派生クラスの作成を担当する方法の1つで、ファクトリークラスという生成専門のクラスにオブジェクトの生成を移譲する方法です。
class CEnemyFactory
{
public:
virtual BOOL Create(smart_ptr<CEnemy> &spEnemy, int EnemyCode)
{
switch(EnemyCode)
{
case 0: // デフォルト
default:
spEnemy.SetPtr(new CEnemy);
break;
case 1: // 強い敵
spEnemy.SetPtr(new CStrongEnemy);
break;
}
return TRUE;
}
メンバ関数であるCreate関数にCEnemyのスマートポインタを渡すと、EnemyCodeに合わせた敵オブジェクトを生成してくれます。敵の種類を増やしたい場合は、CEnemyFactoryクラスを派生させれば良いのです。smart_ptrが無くともファクトリーパターンは作る事が出来ますが、このようにする事で、生成と削除が一揃いとなり、プログラムの安全性が格段に向上します。
smart_ptrクラス及びCom_ptrクラスを作成してしまうと、動的に作成したポインタやCOMインターフェイスを直接やりとりする事がなくなります。よって、クラスの設計自体が大きく変わってしまうと考えられます。この手の根底系クラスはクラス設計の最初の段階から組み込まなくてはいけないので、既存のクラスへの応用は多少面倒にはなりますが、出来ない事はありませんし、その為に必要なメンバ関数も用意されています。
smart_ptrを扱う上での注意は幾つかあります。まず、smart_ptrを扱う限り、そのオブジェクトを自前でdeleteしてはいけません。せっかくsmart_ptrが管理していた参照カウンタを自ら狂わす事になります。new演算子を扱う時には大抵「int *p = new int」としますが、このポインタ変数pが存在すると「delete p」と出来る機会をプログラム上に与えてしまうので、少しイヤな感じがします。ここを「smart_ptr<int> sp(new int)」とすると、pすら出てこないので、確実に削除権をsmart_ptrに移譲でき安全です。
この段階のsmart_ptrで出来ない事が幾つかあります。まずsmar_ptrが保持しているポインタを別の型のsmart_ptrに引き渡す事ができません(同じ型なら可能)。つまり、
smart_ptr<CEnemy> spEnemy;
smart_ptr<CStrongEnemy> spStgEnemy;
spEnemy = spStgEnemy; // ダメ!(代入演算子が定義されない)
spEnemy.SetPtr(spStgEnemy.GetPtr()); // ダメ!(参照カウンタが不正)
という類の代入は出来ません(CStrongEnemyはCEnemyの派生クラス)。別の型のテンプレート同士の代入は、=演算子が定義出来ないので出来ません。ポインタのアップキャストコピーをしようと思ってSetPtr関数とGetPtr関数を組み合わせてもエラーになります。この代入は可能ではあるのですが、SetPtr関数は内部で参照カウンタを「初期化」するので、上の代入が行われた段階で、ポインタ保有者が2つなのに対して、spEnemyが持つ参照カウンタは「1」となります。すると、spEnemyが無くなった段階で、参照カウンタが0となり、spStgEnemyが指す実体を削除してしまい、おかしな事になってしまうのです。ですから、アップキャストを行いたい時には、Upcast関数のような明示的な関数を定義すべきなのでしょうが、使い方が少し面倒にはなります。ヒントとしては「spStgEnemyスマートポインタから参照カウンタへのポインタ変数をコピーする」という関数があれば、実現は可能です。例えばこのような感じでしょうか。
UINT* smart_ptr::GetRefPtr(){return m_pRefCnt;}
void smart_ptr::Upcast(T* ChildPtr, UINT* pRefCnt)
{
// 【面倒なのは以下の点です】
// ChildPtrはsmart_ptr::GetPtr関数で得られるポインタでなければエラー
// pRefCntはsmart_ptr::GetRefPtr関数で得られる参照カウンタへのポインタでなければエラー
// 引数のポインタが同じ場合は意味が無いのでキャストしない
if(ChildPtr == m_pPtr)
return;
// 引数のポインタと参照カウンタを受け継ぐ
Release();
m_pPtr = ChildPtr;
m_pRefCnt = pRefCnt;
// 参照カウンタを増加
AddRef();
}
テンプレートクラスに継承関係が設定できればもっと素直に実装出来るのですが、仕方の無いところです。尚、アップキャストに関するもっと美しくて堅牢な実装については参照文献「Windowsプロフェッショナルゲームプログラミング」にあります。
しかし、スマートポインタはsmar_ptrが保持していないポインタを登録する時にはアップキャストが可能ですから、うまく使い分ければ今のままでも十分使えます。スマートポインタを積極的にプログラムに取り込めば、newとdelete演算子の予測不可能なバグから解放されるはずです。