C/C++ - オブジェクト指向的プログラミング編 第1回 〜std::auto_ptr<>〜

参照権と所有権

オブジェクト指向的プログラミングにおける、多態性を保障するためには、 基底となる仮想クラス、またはインターフェースがポインタ変数、または参照変数の形で、 動的にインスタンスと結合するオブジェクトでなくてはなりません。

もし、ポインタ変数として実装するのならば、インターフェース I、実装クラス CR とするとき、

I* obj=new CR;

などとして構築する必要があり、当然、これは、

delete obj;
obj=NULL;

として解体しなくてはなりません。

問題になるのは、どのタイミングで解体するかというところに集約されます。 当然、アプリケーションが終了するときにまとめて解体すれば済む話ですが、 やたらと大きなインスタンスを構築し続けると、メモリを圧迫します。 いくら、潤沢にメモリがあるからと言って、調子に乗ってると本来のパフォーマンスが 出せなくなります。

参照権

とりあえず、しばらくはポインタ変数について復習していきます。

変数がある有効な記憶領域を参照することが出来るとき、 その変数は参照権を持つと呼ぶことにします。

少なくとも、ポインタ変数が、NULL を右辺値として持つ場合、そのポインタ変数は、 参照権を持ちません。このことから、ここでは、ポインタ変数が右辺値が NULL でなければ、 * 演算子を使うことが出来るため、参照権を持つということにします。

もちろん、厳密な意味では、NULL 以外のポインタを右辺値として持っているにも関わらず、 参照権を持たないポインタ変数など、行儀の悪いプログラミングを していればいくらでも作ることが出来ますが、あくまで、参照権を持たないポインタ変数の 右辺値には NULL が入っていることとします。

ここで、ポインタ変数の代入という操作が何を意味するかを考えると、代入後、2つのポインタ変数は 同じ右辺値を持ちます。すなわち、2つのポインタ変数は同じ有効な記憶領域を参照することとなり、 この瞬間から、この2つのポインタ変数は、参照権を共有することになります。

所有権

では、最初の問題へと戻っていきます。

変数がある記憶領域を解放、または、インスタンスを解体することが出来るとき、その変数は所有権を持つと呼ぶことにします。

当然、free() 関数を使うことが出来る動的変数や delete オペレータを使うことが出来る インスタンスを持つポインタ変数は所有権を持ち、同時に参照権を持ちます。

では、先程と同様に、もし、この所有権を持つポインタ変数を、別のポインタ変数に 代入するとどうなるでしょうか?

先程の話にあったように、まず、2つのポインタ変数は参照権を共有します。 ところが、この瞬間に、いったいどちらが所有権を持つべきかについて、 はっきりとしたことが言えなくなります。

少なくとも、参照権が共有されている状況下においては、そのいずれの変数も、 所有権を持つことが出来ません。参照権の共有がない、 つまり、ただ1つだけのポインタ変数がその解放しなければならない記憶領域を参照している 時に限って、その変数は所有権を持つことになります。

以上のことから、結論を言うと、「ある変数が自分自身に所有権があるかどうかをチェックし、 もし所有権があるときに参照権を放棄する場合、参照先の有効な記憶領域を解放しなくてはならない。」 ということになります。

std::auto_ptr<_Ty> クラス

C++ 標準ライブラリには、所有権を持つポインタ変数を自動的に delete してくれる 便利なクラスがあり、それが、

#include <memory>
namespace std{
 template<class _Ty> auto_ptr;
}

です。

コンストラクタとデストラクタ

使い方は非常に簡単で、コンストラクタにインスタンスを放り込むだけです。

std::auto_ptr<I> obj(new CR);

仕組みは非常に分かりやすくて、obj という自動オブジェクトがスコープを抜けて 解体されるときに、デストラクタ std::auto_ptr<_Ty>::~auto_ptr がプロパティとして持つ インスタンスを delete してくれるというそういう寸法です。

特に、ファクトリのように、関数からインスタンスを構築するような場合に、 インスタンスを return で そのまま返すのではなく、std::auto_ptr でラッピングしてから返すと、 当然、関数スコープを抜けた瞬間に、インスタンスに対する参照権を持っていた ポインタ変数は解体されているため、参照権に共有は起こらず、必ず所有権を持つというわけです。

std::auto_ptr のオペレータ

で、肝心の std::auto_ptr オブジェクトの扱い方ですが、参照権を持つ証拠に、 * オペレータや -> オペレータを使えば、当然、多態性オブジェクトとして振る舞います。

get() メソッド

_Ty* std::auto_ptr<_Ty>::get(void)const;

というメソッドを経由することで、ポインタ変数を引数として要求する関数にも、 渡すことが出来ます。

reset() メソッド

void std::auto_ptr<_Ty>::reset(_Ty* _Ptr);

というメソッドにインスタンスを渡すと、それまで持っていたインスタンスを安全に 解体しつつ、新しいインスタンスをプロパティとして持ちます。

所有権を持つポインタ変数への代入は本来厳禁ですが、reset() メソッドを経由することで、 そのような操作が安全に行えるわけですね。

release() メソッド

もし、所有権を別の変数に渡さなければならないような状況になった場合は、

_Ty* std::auto_ptr<_Ty>::release(void);

とすることで、この戻り値を代入した先の変数に所有権を渡します。 当然、所有権を渡された変数は、責任を持って解放する必要があります。

std::auto_ptr<_Ty>の問題点

一見便利そうに見える std::auto_ptr が、なぜ、スマートポインタと言えないかというと、 結論から言えば、「常に所有権を主張し続けるがために、参照権を共有できないから」と言えます。

実際に、std::auto_ptr のコピーコンストラクタや代入オペレータがどのような実装をしているかというと、 先程の release() メソッドを使いコピー元の所有権を放棄させた上で、自分自身が 所有権を主張します。

そもそも、所有権を放棄させるということは、参照権の共有を許さないわけで、 必然的に、コピー元は、NULL となります。つまり、コピーといいながら、 実際には、移動していることになります。

本来、ポインタ変数を代入することで参照権を共有することそのものが、 ポインタ変数の最大の特徴なわけですが、それを破っているわけですね。

また、地味な問題ではありますが、配列インスタンスは持てません。 配列インスタンスは、delete[] オペレータで解放しなければならないためですね。

つまり、「ポインタのように振る舞いながらも、インスタンスを自動的に解体してくれる」 というスマートポインタを実装するためには、少なくとも、 コピーすればきっちり参照権を共有する仕組みと、いかなる場合でも所有権を持ったなら、 確実にインスタンスを解体する仕組みの2つが必要となります。


文章作成 : yukki-ts (-+-twilight serenade-+- [stage])