この記事は、C++ Advent Calendar 2013 6日目の記事です。
C++11で標準実装された、参照カウンタ方式によるポインタ'shared_ptr'。 古来のC++でもboostライブラリにより利用かのなので、すっかりおなじみのスマートポインターです。 なにせ、newしてもdeleteしなくて良いというお手軽さ。誰からも参照されなくなると勝手にdeleteされるという、慎ましやかな動作で人気を博しています。
しかし、無闇矢鱈と使うと、参照関係が矛盾してしまったり、解放のタイミングが来るって思わぬトラブルに巻き込まれたりすることもあります。 スマートポインタと云えども、正しい設計の下に使うべきでしょう。
ありがちなトラブルの記事は、shared_ptrを要求する邪悪なマネージャーの顛末 を参照してください。
すべてのソースコードが正しい設計とプログラミングを行っているとは限りません。そこで、スコープから抜けたときに、参照が残っていたら通知してくれるshared_ptrを作ってみました。
// std::shared_ptrをラップしたscoped_shared_ptr // スコープから出る時に参照が残っていたら、assertする template <typename T> struct scoped_shared_ptr { shared_ptr<T> value_; template <typename ...Param> scoped_shared_ptr(Param&&... params) : value_(new T(params...)) {} ~scoped_shared_ptr() noexcept { assert(value_.use_count() == 1); // 誰かが握っているので解放できない } template<typename TT> operator shared_ptr<TT> () { return value_; } T& get() { return value_.get(); } };
これは、shared_ptrを要求する関数に、ローカルで作成したクラスオブジェクトを使う時に便利です。
使用例
void foo(shared_ptr<Hoge> ptr); // shared_ptrを要求するapi // コンストラクタで頂いたstringを保持するクラス struct Hoge { string& msg_; Hoge(string& msg) : msg_(msg) {}; ~Hoge() { cout << msg; }; }; void sometask() { string msg("hello"); // hogeは、msgの参照を含んでいるので、sometask()から出たら使えない。 scoped_shared_ptr<Hoge> hoge(msg); // shared_ptrを要求するAPIを呼ぶ。weak_ptrにしてくれば良いのに。(;_;) foo(hoge); } // もし、fooがhogeの参照を解放していなかったら、assertする
これで安心して使えます。わざわざラップしたクラスを作らなくても、出口で assert(hoge.use_count() == 1); とすれば良いですが、scoped_shared_ptrを使った方が例外で飛んで行った時にも拾えるので、安心度が高いです。
この簡単なshared_ptrのwrapperで、思わぬ参照が残っていてアクセス違反による暗黒面への突入は防げましたが、スコープ内でしか使わないのにアロケーターをnewで呼び出してヒープを喰うのは気持ちよくありません。スタックメモリを使った方が遥かに高速ですし、アロケーターを使う事によるフラグメント等の弊害からも守れます。
shared_ptrは、インスタンスを削除するためのデリーターを指定できるので、この機能を使えば簡単にできそうです。 scoped_shared_ptrを make_sharedを使わない様に修正してみました。
// std::shared_ptrをラップしたscoped_shared_ptr。 スコープから出る時に参照が残っていたら、assertする。 template <typename T> struct scoped_shared_ptr { using element_type = T; // std::shared_ptrがクラスオブジェクトを破棄するときに呼ばれる。ここでは何もしない。 struct deleter { void operator() (element_type*) {} }; // 共有するインンスタンスの実体。スコープ内で宣言されたときは、スタック上に生成される。 T body_; // std:shared_ptrの実体。newしたクラスオブジェクトでなく、スタック上に作られたクラスオブジェクトのアドレスを入れる。 std::shared_ptr<T> value_; template <typename ...Param> scoped_shared_ptr(Param&&... params) : body_(params...) , value_(&body_, deleter()) {} // デストラクタでshared_ptrのvalue_は解放されるが、インスタンスはdeleterによって破棄されるので、body_には影響を及ぼさない。 ~scoped_shared_ptr() noexcept { assert(value_.use_count() == 1); } // shared_ptrへのアクセス用 template<typename TT> operator shared_ptr<TT> () { return value_; } };
これで、Hogeが巨大なオブジェクトでもnewされることなく、スタックを食い潰して高速に動いてくれそうです。 しかし、よーく考えると、shared_ptrの内部で、参照カウンターのオブジェクトをnewして作っています。 手元のclang3.4環境で調べてみたところ、48bytesのメモリを標準アロケーターに要求していました。
ゆるせん!
ここまできたら、すべてスタック上のメモリで済ませたくなるのが人情です。 乗りかかった船なので、アロケーターを使わないアロケーターを作る事にしました。
考え方は簡単です。サイズを指定して、そのサイズをスコープ内のローカル変数として確保し、そのメモリ空間から必要なメモリをアサインして渡すアロケーターを作れば良いのです。
試行錯誤すること約1年!(嘘)ホントは1日です。
ようやくできました。以下の様な書式で使えます。
// スタティックアロケータークラス template<size_t BufferSizeByte, typename ElementType> struct static_allocator; // int型で16byteのインスタンスを生成 static_allocator<16, int> alc; // アロケーターオブジェクトの取得 int* i = alc().allocate(1); // int1つ分のメモリがallocatorから確保される。 double* d = alc.get<double>().allocate(1); // 違う型のアロケーターとしても使えます。
こんな感じです。さっそく、scoped_shared_ptrに適用してみました。
// newを一切呼ばずにスコープ内で使えるshared_ptr // インスタンスが破棄されるときに、参照が残っていたらエラーにする template <typename T> struct scoped_shared_ptr { using element_type = T; // インスタンスを解放するためのデリーター。何もしない struct deleter { void operator () (element_type*) {} }; element_type body_; // クラスのインスタンス static_allocator<64,element_type> allocator_; // アロケーター(64bytes確保) std::shared_ptr<element_type> value_; // shared_ptrのインスタンス // コンストラクタ template <typename ...Param> scoped_shared_ptr(Param&&... params) : body_(params...) , value_(&body_, deleter(), allocator_()) // shared_ptrの参照カウンタインスタンスは、allocator()によって確保される。 {} // デストラクタでは、shared_ptrの参照カウントが1(自分自身のみ)でないとエラーにする。 ~scoped_shared_ptr() { assert(value_.use_count() == 1); } // shared_ptr<T>に変換するオペレーター // 派生クラスへの変換を可能にするためテンプレート実装にする template <typename TT> operator std::shared_ptr<TT> () { return value_; } };
参照カウンターのインスタンス確保用に64bytesを固定で確保しているところがイケてないですが、これでメモリアロケーターを使用しないでshared_ptrが使えるようになりまsちした。
ところで、boost::containerには、static_vectorというvector風なコンテナがあります。vectorとの違いは、メモリをアロケートせずに、ローカル宣言で確保して使う事です。
// 使用例 boost::container::static_vector<int, 2> ivec; // int x 2 でバッファを確保 ivec.push_back(1); ivec.push_back(2); ivec.push_back(3); // エラー! 予約したサイズを超える事は出来ない
つまり、今回作成したstatic_allocatorを使えば、普通のvectorをboost::static_vectorと同様に使えるという事です。
static_vectorはboostライブラリにありますが、残念ながらstatic_mapとか、static_unordered_map等は、boost 1.55の時点ではありません。
ならば、作ってしまおう
ということで、作りました。