超賢いポインタ

  • 前口上

C++ではメモリを自分で管理しなければならない。
オブジェクトのライフタイムが単純に決められない場合、
いつオブジェクトを削除するのかを決めるのが難しく、
結局自前でGCのようなものを作ることになってしまう。
(…まあ、そこまであれなケースはめったに無いけど)
単純に消し忘れの危険性があるという問題もある。


そこで、C++の標準的なライブラリではその辺を解決すべく
いくつかのライブラリがある。
まず、STLのauto_ptr。これは単にスコープの終わりで
デストラクトするだけで、スコープとオブジェクトのライフタイム
が一致する場合に消し忘れを防ぐ、程度のものであろう。
次にboostのshared_ptr。これは参照カウンタを用いて
オブジェクトを管理している。
おおむね、shared_ptrはうまく機能するのであるが、
ポインタの参照関係がグラフ状になっている場合はうまく削除できない。


で。そういう場合はどうするのかというと、
weak_ptrなる参照先がこっそり死ぬかもしれないポインタを使ってみたり
そもそも設計を見直してみたり
(…私は循環参照が正当なケースは普通にあると思いますがね)
するわけである。
しかしやはり循環参照の問題を解決できないものかと誰でも一度は(?)
考えるもので、私も2年ほど前から時々考えていた。
で、昨日電車に乗りながらぼーっと考えていたらいけそうな気がしてきて、
やってみたらいけたっぽいような。構想二年、製作二日である。
(あらかじめお断りしておくのだが、あくまでグラフ理論的に
どうこうという話であって、決して実用な物ではないので。)

  • まずは普通のものを

まず、普通のスマートポインタを実装してみる。
これはスマートポインタ間でオブジェクトを共有して、
そこに参照カウンタを持たせて、それを上げ下げしてうんぬん…
というように実装すればよい。
スマートポインタを表すクラスとポインタをラップして
カウンタを持たせるクラスの二つを作る。

template <class T>
class obj{
public:
	obj(T *p):p(p),cnt(1) {}
	~obj() { delete p; }

	void incr() { cnt++; }
	void decr(){
		if (--cnt==0)
			delete this;
	}
	T& ref() { return *p; }

private:
	T* p;
	int cnt;
};

template <class T>
class sptr {
public:
	sptr()           { o=NULL; }
	sptr(T *p)       { o=new obj<T>(p); }
	sptr(sptr<T> &r) { o=r.o; if (o) o->incr(); }
	~sptr()          { if (o) o->decr(); }

	sptr<T> &operator=(const sptr<T> &r){
		if (o) o->decr();
		o=r.o;
		if (o) o->incr();
		return *this;
	}

	T &operator *() { if (!o) throw "nullp_exc"; return  o->ref(); }
	T *operator->() { if (!o) throw "nullp_exc"; return &o->ref(); }

private:
	obj<T> *o;
};

非常に単純にこのように実装できる。
objクラスを受け取ったときにカウンタを増やして、
手放すときにカウンタを減らしている。
カウンタが0になれば勝手に削除される。

使用例は以下の通り。

class aa;
class bb;

class aa{
public:
	aa() {}
	~aa(){ cout<<"aa死す"<<endl; }
	sptr<bb> p;
};

class bb{
public:
	bb() {}
	~bb(){ cout<<"bb死す"<<endl; }
	sptr<aa> p,q;
};

まずこのようなクラスを定義。

void f1()
{
	sptr<aa> pa(new aa());
	sptr<bb> pb(new bb());
}

f1()から抜けると二つはデストラクトされる。

void f2()
{
	sptr<aa> pa(new aa());
	sptr<bb> pb(new bb());

	pa->p=pb;
	pb->p=pa;
}

循環参照を作る。f2()から抜けてもカウンタは0にならないので
デストラクトされない。

void f3()
{
	sptr<aa> pa(new aa());
	sptr<bb> pb(new bb());

	pa->p=pb;
	pb->p=pa;

	pa->p=sptr<bb>();
}

片方の参照を切ってみる。
これだと片方の参照カウントが0になるのでそこからデストラクトされる。

  • そして。より賢いポインタ

は、長くなってきたのでひとまず次回へ、つづく。