今更ながらに Boost.SmartPointers を考える

Smart Pointers というか主に shared_ptr ですがscoped_ptr のこともたまには思い出してあげてね!)
恐らくは散々ガイシュツなネタですが、まー「検索するのが面倒だ」って人の手助けになればいいかと思います。僕も検索するの面倒ですし。
というかこの記事書いてるときもロクに検索してないです。間違いとかあったらすみません。
疑問点とか有りましたら、自分で調べるか、あるいは僕に知らせてもらえると助かります。では、始めましょう。
[最終更新] 応用例にカスタムアロケータでの参照カウント管理を追加: 09/12/16


基本的な使い道

まず基本的なことについては、僕が説明するより、この動画を参照した方が速いでしょう:
http://www.ustream.tv/recorded/2981654
それに対するついったーでの突っ込み:
http://kiwofusi.sakura.ne.jp/hashtag/output.cgi?name=boostjp&start_id=6593771434&limit=1000


参照するのが面倒だって人の為、簡単に要点を説明しようと思いましたが挫折orz
いや、全部が要点なので。詳しくはぜひ動画を見てください。

仕方ないので、ここには要約とは言えないような走り書きを残しておきます:

  • スマートポインタとは、メモリをはじめとする資源の「所有」を管理するシステムである
  • 「所有」が移動こそするものの、各時点では単数の所有者によって管理されている場合(すばる注:実際のリソース管理の多くは このパターン)は、 std::auto_ptr を使う(すばる注:ちなみにC++0xでは std::unique_ptr に改良されるらしいです。あと boost::scoped_ptr も忘れないであげてください)
  • 複数の所有者によって管理されうる場合に使うのが boost::shared_ptr である。所有者が残ってる場合は破棄されず、所有者が無くなった時点で確実に破棄される
  • ただし場合によっては上手くいかない場合もある。循環参照になる場合や「自分自身の shared_ptr が欲しい」場合など。そういう時は boost::weak_ptr を使う(すばる注:今回の随筆では扱いません、あしからず
  • 基本的な機能は以上であるが、 shared_ptr はいろいろな点で工夫がされている
  • まず「型同士の互換性」。 同じ型 T を参照するポインタならば、どんなものでも shared_ptr<T> で扱えるように工夫されている
  • 例として、どっかのライブラリで適当に定義された「俺俺スマートポインタ」でも、きとんと格納できる(すばる注: ちなみに銀天ライブラリで定義されたスマートポインタは to_shared 関数で簡単に shared_ptr に変えられますよ、と宣伝してみる)
  • そのようにする工夫が「デリータ」。コンストラクタで指定することで「破棄」をカスタマイズする(「破棄」とは即ち所有によって生じる義務である)「所有は型に現れない。コンストラクタで所有が決まる」
  • "何を所有しているか"だけが型に来て、"どうやって所有しているか"は型に来させない、ということになるのかな」@kinaba 氏(ついったーログより
  • よーするに、かなり利用範囲は広いhttp://www.kmonos.net/wlog/104.html#_2008091213


・・・とまぁ、こんな感じです。
これらから言える事。つまり shared_ptr は、単純に「参照カウント式スマートポインタ」として捉えるよりは、上に挙げたように「資源の共有管理」という側面から考えて利用することで、より効果的に使うことが出来る、といった感じでしょうか。
そして、そのポイントはカスタムデリータの使い方、と。

応用的な使い方

次に、Boost.勉強会 の後、ついったーで @Cryolite 氏から伺った話を中心に、幾つか紹介しておきます。


まず随筆でちょっとだけ触れたヘルパ関数 make_shared について。
実はこれ、単純なヘルパ関数じゃなくて、凄く効率的みたいなのです。
まー、これは後で本格的に触れるので、今は軽く流します。


次にマイナーコンストラクタである、

template<class Y> shared_ptr(shared_ptr<Y> const & r, T * p);

これについて。
後で語る make_shared の実装を眺めていて発見したコンストラクタです。
これは、Cryolite氏曰く

struct hoge
{
  // このメンバ i を指し示す shared_ptr が欲しい
  // でも、普通には作れない。さて、どうする?
  int i;
};

// 解決策: まず hoge を作って shared_ptr に入れる
boost::shared_ptr<hoge> ph( new hoge() );
// ph->i を shared_ptr に入れるためにはどうするか?
// 前提として ph->i は *ph が削除されたとき自動で削除される
// また ph->i への参照が残っている間に *ph が削除されるのはダメ
// つまり ph と参照カウントを共有する shared_ptr を作り、
// アドレスのみ &(p->i) に設定すればよい、と分かる。
// そのためのコンストラクタがこれ:
boost::shared_ptr<int> pi( ph, &(ph->i) );

この様に使うのだとか。
こうすることで phpi は参照カウントを共用し、もし pi より先に ph が使われなくなっても、きちんと ph に渡した hoge オブジェクトは残ります。
そして phpi 、およびそれらからコピーされた shared_ptr が無くなった時点で、 ph が指していたオブジェクトが破棄される(このとき pi が指していたオブジェクトに shared_ptr 自身が直接何かをするようなことはない。が、そもそもメンバなので同時に破棄される。結果としてリークも二重解放も起きない)。
少し長くなってしまいましたが、こんな感じに振舞うようです。なるほど。


それから、カスタムアロケータ。これは僕が気になって調べました。
shared_ptr はその実装上、参照カウントを生成するときにフリーストア領域にメモリを確保するのですが、これをアロケータを使って最適化できないかという考えです(例えばメモリプールを使う、とか)。

その為のコンストラクタが、三引数を取る、こちら:

template<class Y, class D, class A> shared_ptr(Y * p, D d, A a);

これです。オーバーロードの制約上、デリータ指定必須なのが地味に面倒ですが、まあそこは目をつぶりましょう。

これを使うことで、参照カウントのメモリ管理は、構築時の allocate はもちろん、破棄時の deallocate まで、きちんと shared_ptr 側で管理して貰えます。
例によって shared_ptr を使う側は、そんな処理が裏で行われることを意識する必要が有りません。全く普通の shared_ptr として扱えば、きちんと削除時に所定の deallocate を呼び出してくれます(処理の詳細は型に現れない)。
なお、実際にアロケータによって管理されるのは参照カウントであり、その型はユーザにはよく分かりませんが、その問題は rebind という機能によって解決されています。気になった方は調べてみると良いでしょう。


とりあえず、こんなところでしょうか。
もちろん shared_ptr の応用なんて星の数ほどある筈なので、見つけ次第、この項目に追加していこうかと思っています。
というより、もし有力な shared_ptr の応用を見かけた場合には是非ご一報ください。

実装について

shared_ptr の実装が如何に素晴らしいか、語ろうと思いましたが、
http://d.hatena.ne.jp/setuna-kanata/20081128/1227890991
で基本的なことは語られてるので、そちらに丸投げします。
とりあえず「必読ですよ」とだけ。

make_shared について

さて、本編始まりますmake_shared です。
http://www.boost.org/doc/libs/1_41_0/libs/smart_ptr/make_shared.html


こいつは簡単に言うと「 new演算子呼び出し→ shared_ptr に格納」という一連の流れをスムーズに行うための関数なのですが、実はこいつの本分はそれだけじゃない。
具体的には、

boost::shared_ptr<hoge> p( new hoge() );

と書いた場合、フリーストア領域のメモリ確保は、まず new hoge() 呼び出しで一回、そして p のコンストラクタでの参照カウント生成に一回と、計二回行われます。
しかし、

boost::shared_ptr<hoge> p = boost::make_shared<hoge>();

と書いた場合、フリーストア領域でのメモリ確保は、Cryolite氏曰く、一回しか行われないというのです。


・・・な・・・何を言ってるか分からないでしょうが、僕にも何を言っているか分かりませんでした。最初は。


しかし、ソースコードを読み解くにつれ、なーるほど、と納得。
何が起きているかというと、簡単に言えば、

  1. 使うアイデアは要するに「 shared_ptr に格納されるクラスの本体は、参照カウント内部に用意された Deleter の中に収める」というもの
  2. 最初に、型 T を収めるだけの領域を持たせた Deleter から shared_ptr を構築する。このとき、保持させるポインタの方は、まだ存在しないので 0 にしておく
  3. これにより、 Deleter を中に保持した参照カウンタがフリーストア上に確保される(詳しくは「実装について」で紹介したページを参照)
  4. get_deleterを使って、今作った Deleter への参照を得る(ソースコード上はポインタになってるけど)
  5. placement new を使い、Deleter の領域内に新しい T オブジェクトを構築する
  6. detail::sp_enable_shared_from_this を呼び出す。この関数は T が enable_shared_from_this<T> から派生されていた場合に、きちんと shared_from_this 呼び出しが出来るよう設定してくれるらしい
  7. 先に説明した template shared_ptr(shared_ptr const & r, T * p); のコンストラクタを使い、今構築した T オブジェクトのアドレスと、最初に作った shared_ptr の参照カウントから、新しい shared_ptr を作って返す(この際フリーストア領域確保は行われない)
  8. 最初に作った shared_ptr は、スコープアウトと同時に破棄され、結果として新しいオブジェクトのアドレスと unique な参照カウントを持った shared_ptr が返ることになる

って感じ。ごめん簡単じゃなかったね。
ちなみに
http://d.hatena.ne.jp/Cryolite/20080126#p5
にも同様のことが書いてあるので、そちらも参照。


ここで気をつけるべきことに、 placement new におけるアライメントの問題ってのがあったりするのですが、これは
http://d.hatena.ne.jp/Cryolite/20051102
の記事を参照すると分かります。簡単に言うと「何も考えずに char[sizeof(T)] で領域確保しちゃダメ」って考えておけばよいようです。代替手段として boost::aligned_storage を使う、と。


このことにさえ気付けば、boost/make_shared.hpp のソースコードを読み解くことも十分にできると思いますので、興味のある方は、ぜひ読んでみてください。


なお「これじゃ new 演算子オーバーロードし最適化した場合に困るじゃん」と思うかもしれませんが、きちんと Allocator を用いた boost::allocate_shared ってのも存在します。
ちなみにこれは、アロケータの rebind を行う良い例だと思うので、そちらも興味があれば。


長くなってしまいましたが、要するに僕が言いたかったことは単純。
みんなも積極的に boost::make_shared を使おうぜ!
ということです。効率だけでなく、例外安全性も高まりますよ!