munepi.com > Article > auto_ptrはダメなのか?

auto_ptrはダメなのか?

なぜスマートポインタを使いたいのか

以前書いたboost::shared_ptrの記事を読みかえしてみました。 なんともひどい文章だ。何より内容が分かりづらいのが良くない。ということで、もう一度スマートポインタについて整理してみようと思います。

スマートポインタについて考える前に、 そもそもポインタをそのまま使うのが何故いけないのでしょうか。「ポインタが何か」についてはもう分かっているとして、 ポインタを使わなければいけない局面を考えてみましょう。

ポインタがどうしても必要で、かつ真価を発揮するのは動的なメモリ確保と一緒に使うときでしょう。C++ではnew演算子で動的なメモリ確保を行えます。

int main(int argc, char* argv[])
{
  BIGSTRUCT* ptr = new BIGSTRUCT;
  ...
  return 0;
}

とりあえず、BIGSTRUCTが何かということや、 1個くらい静的に作ればいいだろ!という突っ込みはしないことにしておきます。

でも、このコードだとすでにアウトですね。delete演算子で使い終わったメモリを解放しないとメモリリークです。

# Windows98以前ならこんなんで落ちたりするんだな・・・(^^ゞ

int main(int argc, char* argv[])
{
  BIGSTRUCT* ptr = new BIGSTRUCT;
  ...
  delete ptr;
  return 0;
}

で、このdeleteはとかく忘れやすい。 人間って自分勝手ですね。使ったものはきちんと返せばいいのに・・・。

そこで、メモリの管理機能を付加したメモリであるスマートポインタを使おうということになるわけですね。C++標準では、std::auto_ptrがあります。

#include <memory>

int main(int argc, char* argv[])
{
  std::auto_ptr<BIGSTRUCT> ptr(new BIGSTRUCT);
  ...
  return 0;
}

deleteが消えました。auto_ptrはスコープをはずれると管理しているポインタを自動的に deleteします。便利ですね。

引数にするときは・・・

ポインタは関数の引数として使うことが多いでしょう。

#include <memory>

void foo(BIGSTRUCT* ptr_foo)
{
  ptr_foo->...; // なんらかの操作
}

int main(int argc, char* argv[])
{
  std::auto_ptr<BIGSTRUCT> ptr(new BIGSTRUCT);
  foo(ptr.get());
  ...
  return 0;
}

auto_ptrはそのままポインタ型(BIGSTRUCT*)としては渡せず、 get()関数を呼ぶ必要があります。イヤですね。透過的でありません。でも、これは意図的なのです。

次のコードはアウトです。

#include <memory>

void bar(std::auto_ptr<BIGSTRUCT> ptr_bar)
{
  ptr_bar->...; // なんらかの操作
}

int main(int argc, char* argv[])
{
  std::auto_ptr<BIGSTRUCT> ptr(new BIGSTRUCT);
  bar(ptr);
  ...
  return 0;
}

引数の型をauto_ptr<BIGSTRUCT>にすれば bar()にptrを直接渡せますが、不正です。

bar()内では、 引数として渡されたptrから新しいptr_barにメモリのアドレスが渡されます。しかしこれがくせ者で、 bar()から抜けるときにはこのptr_barが削除されてしまうため、 管理していたメモリも削除されます。 つまり、bar()から帰ってきた main()内ではもうptrの持っているアドレスにはメモリがありません。もしbar()の呼び出しの後に ptrを使おうと思っても不正なアドレスの参照になってしまうことでしょう。

# これでまたWindowsをいじめられる。

こういうことを避けるために、 auto_ptrは get()を必要とする仕様になっているのでしょう。

コンテナに入れるときは・・・

配列を作る場合はauto_ptrは使えません。

#include <memory>

int main(int argc, char* argv[])
{
  std::auto_ptr<BIGSTRUCT> ptr(new BIGSTRUCT[]); // だめ!!
  ...
  return 0;
}

配列はdelete[]とする必要があるからです。

じゃぁ、vectorを使いましょうか。

#include <memory>
#include <vector>

int main(int argc, char* argv[])
{
  std::vector<std::auto_ptr<BIGSTRUCT> > ptrs;
  ptrs.push_back(std::auto_ptr<BIGSTRUCT>(new BIGSTRUCT));
  ...
  return 0;
}

ところがこれは不可です。

STLコンテナであるvectorでは、 コピーコンストラクタを使って要素を追加しますが、 constなオブジェクトをコピーできなくてはいけません。でも、auto_ptrはコピー後にはコピー元から所有権を奪う(つまりポインタの所有者は一人=削除の責任を持つ)設計のため、 constな(引数に変化を及ぼせない)コピーコンストラクタはありません。

VC6ではconstなコピーコンストラクタがあるので、 auto_ptrがSTLコンテナに入れられます(古い仕様らしい)。 VC.NETでは変わったそうです。BCC5/5.5は正しい仕様に基づいています。 が、ドキュメントが間違っています(constになってる)。

共有カウント付きスマートポインタ

そこでやっとこ登場してくるのがboost::shared_ptr。所有しているアドレスがいくつのshared_ptrから参照されているかを数えていて、 所有者が無くなった時点でメモリを削除します。

コピーは所有者を増やすだけのでOK。 相手の変更は必要ないのでconstでもいい。ということでこうなります。

#include <boost/shared_ptr.hpp>
#include <vector>

void bar2(boost::shared_ptr<BIGSTRUCT> ptr_bar)
{
  ptr_bar->...; // なんらかの操作
}

int main(int argc, char* argv[])
{
  std::vector<boost::shared_ptr<BIGSTRUCT> > ptrs;
  ptrs.push_back(boost::shared_ptr<BIGSTRUCT>(new BIGSTRUCT));
  ...
  bar2(ptrs[0]);
  
  return 0;
}

bar2もOKです。便利ですね。

ということで、コンテナを使う場合や関数に渡す場合はshared_ptrを、 ローカル関数のなかでだけ使う場合・・・例えば一時作業領域をつくる時などはauto_ptrを使えばいいでしょう。

shared_ptrだけを使うようにしても良いわけですが、 auto_ptrには、動作が軽く標準でついてくるという強みもありますので使い分けをするといいのではないでしょうか。

今日の参考サイト