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) );
この様に使うのだとか。
こうすることで ph
と pi
は参照カウントを共用し、もし pi
より先に ph
が使われなくなっても、きちんと ph
に渡した hoge
オブジェクトは残ります。
そして ph
と pi
、およびそれらからコピーされた 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氏曰く、一回しか行われないというのです。
・・・な・・・何を言ってるか分からないでしょうが、僕にも何を言っているか分かりませんでした。最初は。
しかし、ソースコードを読み解くにつれ、なーるほど、と納得。
何が起きているかというと、簡単に言えば、
- 使うアイデアは要するに「
shared_ptr
に格納されるクラスの本体は、参照カウント内部に用意された Deleter の中に収める」というもの - 最初に、型 T を収めるだけの領域を持たせた Deleter から
shared_ptr
を構築する。このとき、保持させるポインタの方は、まだ存在しないので0
にしておく - これにより、 Deleter を中に保持した参照カウンタがフリーストア上に確保される(詳しくは「実装について」で紹介したページを参照)
get_deleter
を使って、今作った Deleter への参照を得る(ソースコード上はポインタになってるけど)- placement new を使い、Deleter の領域内に新しい T オブジェクトを構築する
detail::sp_enable_shared_from_this
を呼び出す。この関数は T がenable_shared_from_this<T>
から派生されていた場合に、きちんとshared_from_this
呼び出しが出来るよう設定してくれるらしい- 先に説明した
template
のコンストラクタを使い、今構築した T オブジェクトのアドレスと、最初に作ったshared_ptr(shared_ptr const & r, T * p); shared_ptr
の参照カウントから、新しいshared_ptr
を作って返す(この際フリーストア領域確保は行われない) - 最初に作った
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
を使おうぜ!
ということです。効率だけでなく、例外安全性も高まりますよ!