C++11 スマートポインタの話

C++にはガーベジコレクタが言語的には存在しないので、動的に確保したオブジェクトのメモリの管理はプログラマが責任を持って管理しなければ、メモリリーク・リソースリークなどの問題を生じてしまいます。

 

今時のプログラマはこうした問題を引き起こすリスクを減らす為に、記述ミスの起こりやすい生のポインタを使い自力でnewとdeleteを記述する事を避けて、安全なスマートポインタ、即ちオブジェクトの寿命により自動的にメモリ解放を行う便利なポインタを利用するべきです。

 

しかし、スマートポインタと言っても種類が幾つかあり、落とし穴も存在するのでどういった状況でどれを利用するのが良いのかを考察してみます。

 

Dynamic memory managementを見てみても C++11から随分増えていますね

http://en.cppreference.com/w/cpp/memory

 

 

std::auto_ptr

 

まずはauto_ptrです。このスマートポインタは、オワコンなので使ってはいけません。(deprecated)

いかに、auto_ptrを駆使したテクニックがあるとしてもこれから使うべきではありません。

このスマートポインタは幾つかの欠点を抱えている上、非推奨要素は規格からいずれ削除されてもおかしくないからです。

 

説明の為にとりあげますが、このスマートポインタには先に述べた通り幾つかの問題があります。

何年か前に書かれたコードで見かける事があるかもしれません。

 

std::auto_ptr<hoge> ptr(new hoge); 

 

auto_ptrはスコープを外れると管理しているポインタを自動的にdeleteします。

これにより、プログラマはdeleteを書き忘れメモリがリークする事を未然に防ぐ事が出来ます。

 

auto_ptrには所有権が存在します。そして、同じポインタに対して所有権を持つauto_ptrはたった1つでなければなりません。auto_ptr型の変数をコピーするとそのauto_ptrが管理しているポインタがコピー先に移動し、コピー元のauto_ptrはnullptrを指すようになります。即ちコピーによって同一のオブジェクトを指すauto_ptrが増える事はなく、必ず1つのauto_ptrのみに保たれます。このようなコピーを破壊的なコピーと呼び、これがauto_ptrの所有権の決まりを成立させています。

 

しかし、コピー以外の方法で所有権の決まりを無視して同一のオブジェクトを指す複数のauto_ptrを作成する事が可能で、その場合同じオブジェクトの削除が複数回行われ例外を発生させてしまいます。

また、破壊的なコピーの問題から操作の中でコピーを用いる可能性があるコンテナに格納してはいけないといったデメリットがあります。

他にも削除時の動作を指定出来ないなど、デメリットの詳細に関しては少し検索すれば沢山出てくるかと思います。

 

これらのデメリットを克服したstd::unique_ptrスマートポインタが既にあるので、このスマートポインタは用いるべきではありません。

 

 

std::unique_ptr

 

auto_ptrの欠点を補ったスマートポインタです。

コンテナに入れる事も出来、カスタムデリータを使用する事も出来ます。

std::unique_ptr< new hoge(), custom_deleter > hoge_obj;

 

auto_ptrの代わりにこちらを使うべきです。

 

 

std::shared_ptr

 

std::shared_ptrは同様のObjectを指すshared_ptrが複数存在する事を許容するスマートポインタです。std::shared_ptrは現在幾つのshared_ptrが同じオブジェクトを指しているかのカウンタを持ちます。あるオブジェクトを指すshared_ptrが増えた時にカウンタを1つ加算し、減った時にカウンタを1つ減算します。最終的にどこからも参照されなくなったオブジェクトにアクセスする方法が存在しないので、カウンタが0になった際に自動的に対象オブジェクトをdeleteしてくれます。これによってメモリリークを防ぐ事が出来ます。

 

非常に便利ですが、循環参照してしまった場合に解放されないという点に気をつけて下さい。

{

std::shared_ptr<loop> p1(new loop);

std::shared_ptr<loop> p2(new loop);

 

p1->ptr = p2;

p2->ptr = p1;

} // スコープを抜けてもメモリは解放されずリークする

 

この問題の解決には後ほど記述するstd::weak_ptrを用います。

 

また、std::shared_ptr<loop> p1(new loop);という方法でメモリを確保すべきではありません。

これに関しても後ほど記述するstd::make_sharedで解説します。

 

 

std::weak_ptr

 

std::weak_ptrは、既にshared_ptrにより管理されているオブジェクトへの弱参照を保持します。

これはshared_ptrと違い、参照カウントに関わり無くスコープを抜けるとメモリを解放してくれます。

これによって循環参照の問題を解決する事が出来ます。

 

この問題を取り上げたこちらの記事が分かりやすいです。(というか言いたいことが書いてあります)

http://d.hatena.ne.jp/naokirin/20110124/1295859711

 

 

std::make_shared

 

先ほど、std::shared_ptrの項で、std::shared_ptr<loop> p1(new loop);という方法でメモリを確保すべきではありませんと書きました。

その理由に関して説明します。

 

http://en.cppreference.com/w/cpp/memory/shared_ptr/make_shared

に説明があったので簡単に訳すと

 

template< class T, class... Args >

shared_ptr<T> make_shared( Args&&... args );

 

この関数は一般にT型のオブジェクトとそのshared_ptrの為のブロックの確保を1度のメモリ割り当て操作で行う事が期待されます。

これに対して、std::shared_ptr<T> p(new T(Args...))の記法を用いた宣言では少なくとも2度のメモリ割り当て操作が行われ、不要なオーバーヘッドを招くと考えられます。

加えてf(shared_ptr<int>(new int(42)), g())という記述はメモリリークを起こす可能性があります。これもmake_sharedを用いれば起こりません。

 

つまり、std::shared_ptrに対して動的なメモリ確保を行う場合は、パフォーマンス面、安全面からサンプルコードにあるように

auto sp = std::make_shared<int>(10); 

といった風にstd::make_sharedを用いるべきなのです。

 

 

std::allocate_shared 

 

http://en.cppreference.com/w/cpp/memory/shared_ptr/allocate_shared

 

template< class T, class Alloc, class... Args >

shared_ptr<T> allocate_shared( const Alloc& alloc, Args... args );

 

const Alloc& allocで渡したアロケータを使えるmake_sharedと考えて良いと思います。

 

自分もまだまだ理解が足りてないように思うので、C++のこわい方々は、この記事に間違いや補足などあればコメントで指摘してくださるとありがたいです。