ウィークポインタテンプレートクラス
マルペケではオブジェクトの削除を自動化するスマートポインタを提供しております。スマートポインタについてはこちらをご覧下さい。
スマートポインタの最大の魅力は、newで確保したメモリ(オブジェクト)の参照数をチェックして、誰も参照しなくなった瞬間に自動的にそのメモリを解放してくれる点です。例えば、
void func() {
sp< int > spVal( new int ): // int型のメモリを確保してスマートポインタに格納
}
とすると、spValにヒープメモリから確保されたint型のポインタが格納されます。func関数を抜けるとspのデストラクタが自動的に呼ばれます。この時、自分が保持していたポインタ先にあるメモリも一緒にdeleteしてくれます。これによりもはやnewしたオブジェクトに対してdeleteを呼ぶ行為を忘れて良くなります。デストラクタが自動的に呼ばれるという性質を利用した恐ろしく便利なヒープメモリ管理人、それが「スマートポインタ」です。
このように大変便利なスマートポインタですが、実は「循環参照問題」という唯一にして最大の弱点があります(それについてはすぐ下で説明します)。この問題にはまると、何と保持したヒープを消してくれなくなるんです。つまりメモリリークが起こってしまいます。とっても困ってしまう問題なんですが、この章の題目である「ウィークポインタ」は、その弱点をカバーしてくれます(直接解決はしません)。ウィークポインタを知ると、ウィークポインタ無しにスマートポインタを怖くて使えなくなります。どういう事なのか、本章で見ていきましょう。
@ スマートポインタ最大の弱点「循環参照問題」とは?
スマートポインタには「循環参照するとメモリリークが発生する」という超致命的な弱点があります。典型的な例を挙げます:
#include "sp.h" // スマートポインタ
#include <crtdbg.h>
struct SB;
struct SA {
sp<SB> Bobj; // 相手SBのスマートポインタを保持
};
struct SB {
sp<SA> Aobj; // 相手SAのスマートポインタを保持
};
void func() {
sp<SA> spA( new SA ); // ヒープメモリ保持
sp<SB> spB( new SB ); // ヒープメモリ保持
// お互いに持ち合うとメモリリーク!!
spA->Bobj = spB;
spB->Aobj = spA;
}
int _tmain(int argc, _TCHAR* argv[])
{
// リーク検出
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
func();
return 0;
}
SA及びSBという2つの構造体が定義されていて、お互いにお互いのスマートポインタを保持する構造になっています。func関数内で実際にお互いのスマートポインタを持ち合っています。この状態でアプリケーションを終了させると、メモリリークが発生してしまいます!スマートポインタが誤作動してしまうんです。
なぜそうなってしまうのか?func関数内の参照カウンタの動きを追ってみましょう:
spA SA.Bobj spB SB.Aobj RefCnt RefCnt RefCnt RefCnt スマートポインタspAにSA格納 1 0 0 0 スマートポインタspBにSB格納 1 0 1 0 spA->Bobj = spB と代入 1 2 2 0 spB->Aobj = spA と代入 2 2 2 2 spAがスコープ外に。参照カウンタが1なので保持しているSAは
消えない。つまりSA保持しているBobjも消えないのでSBの
参照カウンタはそのまま。1 2 2 1 spBがスコープ外に。参照カウンタが1なので保持しているSBは
消えない。つまりSBが保持しているAobjも消えないのでSAの
参照カウンタはそのまま。1 1 1 1 関数から抜けたが、お互いが消せていない!!(リーク発生) 1 1 1 1
黄色及び緑色は共有しているカウンタを表します。spAとspBはそれぞれの仕事を正しくしているつもりなのですが、お互いに相手の削除を待ち合ってしまうため、結果としてメモリリークが発生してしまっています。これはスマートポインタが採用している参照カウンタ方式の潜在的な性質であるため、防ぐ事はできません。
厄介なのは、3つ以上のクラスで循環しても起こってしまいます。例えばAがB、BがC、CがAと循環して持つ状態になってもリークしてしまいます。これが網の目のような複雑な設計で起きたら…恐ろしいわけです。でも、そういうのはゲーム製作にいくらでもあります:
スマートポインタのようにそれぞれがオブジェクトへのポインタをがっつり共有し、誰でもそのオブジェクトを削除する権限を持つ物は「強参照」と呼ばれます。削除責任があるために参照カウンタが必要になり、結果として循環参照問題にはまってしまうわけです。そこで、この「削除する」という権利を放棄する方法が考えられました。削除はしないけどもポインタの参照はしたい。このゆるい参照は「弱参照」と呼ばれます。まさに「ウィークポインタ(Weak
Pointer)」です。
A ウィークポインタの仕組みとは?
ウィークポインタは保持しているポインタ先を「消さない」という責任放棄をする事で、スマートポインタの持つ循環参照問題を回避します。その仕組みを見てみましょう。
ウィークポインタをいきなり作るという事はできないんです。つまり、
wp<SA> wpA( new SA ); // コンパイルエラー
とウィークポインタに直接生ポインタを渡す事はできません。そういうコンストラクタが存在しません。ウィークポインタが受け付けるのは「スマートポインタ」か「すでに作成されたウィークポインタ」です:
sp<SA> spA( new SA );
wp<SA> wpA( spA ); // OK
wp<SA> wpACopy( wpA ); // OK
実は、ウィークポインタはスマートポインタから作成されます。そのため、ウィークポインタを実現するにはスマートポインタを拡張する必要があります。既存のスマートポインタの内部には参照カウンタがありますが、そこにさらに「ウィークカウンタ」を追加します。そして次のルールに従って両方のカウンタを増減させます:
・ スマートポインタを作成した時は参照カウンタとウィークカウンタの両方を増やす。
・ スマートポインタをコピーした時は両方のカウンタを増やす。
・ スマートポインタを削除した時は両方のカウンタを減らす。
・ ウィークポインタにスマートポインタを代入した時は、スマートポインタから生ポインタとウィークカウンタをコピーし、ウィークカウンタのみ増やす。
・ ウィークポインタをコピーした時はウィークカウンタだけを増やす。
・ ウィークポインタを削除した時はウィークカウンタだけを減らす。
・ 参照カウンタが0になったらスマートポインタはオブジェクトのみを削除し、参照カウンタは残す。
・ ウィークカウンタが0になったらスマートポインタもしくはウィークポインタのどちらかが参照カウンタとウィークカウンタの両方を削除する。
これで循環参照を防ぐ事ができるんです。試しに先ほどの構造体SA及びSBが持っていたお互いのスマートポインタをウィークポインタに変更してみます:
#include "sp.h" // スマートポインタ
#include <crtdbg.h>
struct SB;
struct SA {
wp<SB> wpBobj; // 相手SBのウィークポインタを保持
};
struct SB {
wp<SA> wpAobj; // 相手SAのウィークポインタを保持
};
void func() {
sp<SA> spA( new SA ); // ヒープメモリ保持
sp<SB> spB( new SB ); // ヒープメモリ保持
// お互いに持ち合う
spA->wpBobj = spB;
spB->wpAobj = spA;
}
int _tmain(int argc, _TCHAR* argv[])
{
// リーク検出
_CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF);
func();
return 0;
}
上のfunc関数内でのそれぞれのカウンタの動きを追ってみます:
spA spA.wpBobj spB spB.wpAobj RefCnt WeakCnt RefCnt WeakCnt RefCnt WeakCnt RefCnt WeakCnt スマートポインタspAにSA格納(両方増やす) 1 1 0 0 0 0 0 0 スマートポインタspBにSB格納(両方増やす) 1 1 0 0 1 1 0 0 spA->wpBobj = spB と代入。
spBの持つウィークカウンタのみ増加。
wpBobjにウィークカウンタをコピー。1 1 1 2(copy) 1 2 0 0 spB->wpA = spA と代入。
spAの持つウィークカウンタのみ増加。
wpAobjにウィークカウンタをコピー。1 2 1 2 1 2 1 2(copy) spAがスコープ外に。両方のカウンタを減らす。
0 1 1 2 1 2 0 1 spAの参照カウンタが0になったのでspA.wpBobjは消える(wpBobjのウィークカウンタのみ減らす)。ウィークカウンタが残っているのでspAの参照カウンタは消されない。
0 1 1 1 1 1 0 1 spBがスコープ外に。両方のカウンタを減らす。
ウィークカウンタが0になったので、spBの参照カウンタとウィークカウンタの両方を消す。
0 1 0(del) 0(del) 0(del) 0(del) 0 1 spBの参照カウンタが0になったのでspB.wpAobjは消える(wpAobjのウィークカウンタのみ減らす)。
ウィークカウンタが0になったので、spAの参照カウンタとウィークカウンタの両方を消す。0(del) 0(del) 0(del) 0(del) すべてのカウンタが消えている(もちろんオブジェクトも)
双方のカウンタが見事に全部ゼロになり、参照カウンタが0の時に保持しているオブジェクトメモリも解放されているため、関数を抜けてもメモリリークは起こりません。循環参照してもメモリリークが起こらない。これがウィークポインタの力です。
B 実装
ウィークポインタを実装するには、スマートポインタにウィークカウンタを新設する必要があります。後はAのルールに従ってオブジェクトの消去とカウンタの消去を別々に行えるようにすればOKです。
詳しい実装は煩雑になりますのでマルペケで提供しているスマートポインタをご参照下さい。
C 使い方に注意、甘い話にはやっぱり罠がある…
ウィークポインタは循環参照問題が起こらない。これは嬉しい性質です。しかし、ウィークポインタは参照しているメモリの解放責任を放棄しています。では、大元のスマートポインタが無くなったらどうなるのでしょうか?
void func() {
sp<MyClass> spObj( new MyClass );
wp<MyClass> wpObj( spObj );
spObj = 0; // spObjは消える!
int v = wpObj->val; // error! 参照エラーが起こる!!
}
スマートポインタが無くなった段階で、保持されているオブジェクトは解放されてしまいます。でもウィークポインタは存在していて、内部にはオブジェクトの生ポインタが握られています…ですから、上のようなアクセスであっさりと参照エラーが起きてしまいます。ウィークポインタは参照先のオブジェクトの存在を保証しません。「えー、じゃぁウィークポインタって何なの?」となります。ウィークポインタとは「参照しているポインタが『存在しているか』を確認する術を持つポインタ」なんです。
ウィークポインタが存在している時、オブジェクトは無くなっているかもしれませんが、参照カウンタとウィークカウンタは生きています。両方のカウンタはウィークカウンタが0になった時に始めて消されるのでした。参照カウンタは常にウィークカウンタ以下です。つまり参照カウンタが0の時にはオブジェクトが存在していない、1以上なら存在していると判断できるんです。上の危ない参照を避けるには:
void func() {
sp<MyClass> spObj( new MyClass );
wp<MyClass> wpObj( spObj );
if ( wpObj->isExist() )
int v = wpObj->val; // OK
spObj = 0; // spObjは消える!
if ( wpObj->isExist() )
int v = wpObj->val; // ここには来ない!
}
と参照先の存在をチェックします。これはウィークポインタを使う上で必ずしなければなりません!!
D ウィークポインタ公開
本章で取り上げたウィークポインタはゲームつくろ〜ツール編その6「スマートポインタ」のv2.20以降で公開しております。ご自由にお使い下さい。