ポインタは用法用量を守って正しくお使いください

この記事は C++ Advent Calendar jp 2010 の参加記事です。

C++にはポインタ(参照のセマンティクスを持つデータ)がたくさんありますが、悲しいかな生ポインタ(raw pointer/Cのポインタ)が簡単(見た目的な意味で)過ぎて、ほかの便利で安全で高速なポインタはなかなか使ってもらえないようです。
そこで、本稿では「ほげほげポインタ」の種類と使い分けについて簡単に解説します。

生ポインタ

概要

みんな大好き(一部の人は大嫌い)なCのポインタ。なんでもできる万能選手ですが、なんでもできるが故に実行時エラーの温床となりやすい危険な存在です。生ポインタを見かけたら実行時エラーがあると思え!

特徴
  • コピー: 可(Trivially Copyable)
  • ムーブ: 可(コピーと同じ)
  • 所有権管理: なし(リーク・二重デリートし放題)
補足

生ポインタが危険な原因は、「複数の概念を一つの言語機能で表現している」ことに尽きます。生ポ一つで「弱参照」「イテレータ」「Nullable」といった機能を実現することができてしまいます。その上所有権管理が出来ないので、不注意が無くてもリソースリークや二重デリート・範囲外アクセスが起こってしまいます。どうしても必要な時(C APIとのインタフェースや低レベルなライブラリルーチン)以外は使わないようにしましょう。

std::auto_ptr

概要

C++98唯一の標準スマートポインタ。「所有権を自動的に移譲する」という動作(ムーブ)をします。コピーは不可能なので、生ポの代わりとしてはいまいちです。主な使い道は、ライブラリ中の「リソースを確保してポインタを返す関数」の戻り値としてT *の代わりにauto_ptrを返す、などでしょうか。

特徴
  • コピー: 不可
  • ムーブ: 可(auto_ptr &を取るコピーコンストラクタが用意されている)
  • 所有権管理: 単一の強参照(有効なポインタを保持しているオブジェクトが破棄される時にポインタの参照先をdeleteする)
補足

使い勝手はあまりよくない上に、C++0xでは非推奨扱いとなったかわいそうな子ですが、それでもすべての標準ライブラリに入っているので、移植性を重視するときにはお世話になると思います。

boost::shared_ptr

概要

スマポと言ったらコレ、というくらいには定番ですね。どこからも参照されなくなった時点でデストラクタが迅速に呼ばれる、デストラクタの呼び出し地点でデストラクタの定義が見えていなくてもいい、バイナリ境界を安全に超えられる*1、といった特徴があり、基本的に使わないという選択肢は無いと思います。尚、配列を使う場合はboost::shared_arrayをどうぞ。

特徴
  • コピー: 可(参照カウントにより所有元の数をカウント)
  • ムーブ: 可(コピーと同じ)
  • 所有権管理: 複数の強参照(参照カウンタがゼロになった時点で、登録されているデリータを呼び出す)
補足

たまに「循環参照が〜」とか言って文句つける人が居ますが、冷静に考えてみてください、循環参照を発生させた時点でデストラクタの安全な実行が出来ません(片方を先に破棄すると、もう片方のオブジェクトはダングリングポインタを持つことになる*2 )。デストラクタを前提としたC++ではそもそも特別な仕組みを使わないと循環参照の解決は出来ない*3のです。

boost::scoped_ptr

概要

ローカルスコープ内でのみ有効なポインタです。コピー不可、ムーブも無いので他のスコープに渡すことはできません。が、逆に生ポインタと比較してオーバーヘッドはありません。主な使い道はクラスのメンバ変数でしょうか(テンポラリバッファはstd::vectorを使いましょう)。unique_ptr(後述)の登場により随分と影が薄くなりました。配列の場合はshared_と同じくboost::scoped_arrayを使いましょう。

特徴
  • コピー: 不可
  • ムーブ: 不可
  • 所有権管理: 単一の強参照(破棄時にdeleteを呼ぶ)
補足

画面内にnewもdeleteも見えてるから生ポでいいよ!という場面でも、scoped_ptrにしておくと例外安全を別に考慮する必要が無くなります。「例外は 忘れたころに 飛んでくる」

std::shared_ptr

概要

C++0xで登場するスマートポインタ。boost::shared_ptrのほぼべた移植。ただしBoost版と違い、default_deleteがTに対して特殊化されているので、配列の場合はstd::shared_ptr>とすればOKです。Tで使えるのはunique_ptrだけでした。配列に対してはshared_ptr(new T[n], default_delete>())とする必要があるようです。まあshared_ptr>で十分ですよね。

特徴
  • コピー: 可(参照カウントにより所有元の数をカウント)
  • ムーブ: 可
  • 所有権管理: 複数の強参照(参照カウンタがゼロになった時点で、登録されているデリータを呼び出す)
補足

構築時のコストを減らすためにmake_sharedを使うとか、そこら辺の話はid:gintenlabo先生あたりが何か書いていたような。

std::unique_ptr

概要

C++0xで登場するスマートポインタ。boost::scoped_ptrの超強化版です。ムーブがサポートされたので、ローカルスコープに限らず、「複数のオブジェクトに同時に所有されない」を満たすすべての状況で使用可能です。当然ゼロオーバーヘッド(生ポ比)、また、カスタムデリータ(operator deleteの代わりにデストラクト時に呼ばれる関数)も使えるので、メモリ以外のリソース(ファイル・ソケット等)にも使えます。

特徴
  • コピー: 不可
  • ムーブ: 可
  • 所有権管理: 単一の強参照(破棄時にdeleteを呼ぶ)
補足

個人的にはstd::shared_ptrよりも使用頻度が多くなる気がします。デリータがあるので、C APIで確保したリソースの管理にも使えます(見方によっては、「インラインでリソース管理クラスを記述できるクラステンプレート」と考えることもできる)。また、unique_ptrの特殊化も用意されているので、配列もOKです。C++98で使いたい場合はboost::interprocess::unique_ptrがあります。

まとめ

C++はポインタがたくさんあって幸せですね!どれ使うか迷った人はとりあえずunique_ptr、コピーしたくなったらshared_ptrを使うようにすればいいでしょう。unique_ptrは人間がdeleteやtry-catchを手動で書くのと同じコード(以上でも以下でもない)を自動で生成してくれるので、追加コストを気にすることなく安全性・利便性を手に入れられます。またshared_ptrは参照カウントと型消去を行うので(特にスレッド間で共有されている場合は)コピーにわずかな追加コストがかかりますが、それを補って余りある機能を持っているので、大いに活用しましょう!
なんだか適当に書いた感溢れる記事ですがそれはきっと気のせいです。id:coiledcoil先生に続きます。

*1:id:Cryolite先生の伝説のセッション参照

*2:他の言語では、ファイナライザでの所有オブジェクトに対してのアクセスを禁止したり等で回避しているようです

*3:weak_ptrによる循環参照の解決については詳しい人に任せます