読者です 読者をやめる 読者になる 読者になる

スマートポインタの使い方 その2:shared_ptr

C++ Tips

shared_ptrを使ってみる

unique_ptrが変数の寿命が尽きた段階でメモリ領域を開放するのに対し、shared_ptrは参照カウンタを持ち参照カウンタがゼロになるとメモリ領域を開放する。

#include <tchar.h>
#ifdef __BORLANDC__
#include <boost/tr1/memory.hpp>
namespace std
{
  using namespace tr1; // 他のコンパイラと同様にstd::tr1をstdとする
}
#else
#include <memory>
#endif
#include <iostream>

// 円を表す図形
class CPrimitiveCircle
{
public:
  CPrimitiveCircle() :
    x(0), y(0), r(0)
  {
  }
  CPrimitiveCircle(int xx, int yy, int rr) :
    x(xx), y(yy), r(rr)
  {
  }
  int x, y, r;
};

int _tmain(int argc, _TCHAR* argv[])
{
  std::shared_ptr<CPrimitiveCircle> c1(new CPrimitiveCircle(10, 20, 100));

  // 振る舞いはあくまでもポインタ
  std::cout << "count = " << c1.use_count() << " r = " << c1->r << std::endl;

  std::shared_ptr<CPrimitiveCircle> c2 = c1; // 参照回数が増える
  std::cout << "count = " << c1.use_count() << " r = " << c1->r << std::endl;

  std::shared_ptr<CPrimitiveCircle> c3 = c1; // 参照回数が増える
  std::cout << "count = " << c3.use_count() << " r = " << c3->r << std::endl; 

  return 0;
}

あくまでも「ポインタ」として振る舞うので、メンバのアクセスはアロー演算子(operator->)で行う。use_countメソッドはshared_ptrが管理しているメモリ領域の参照数を取得する。
比較も同様。operator==とoperator!=で管理しているメモリ領域が「同一」か否かもチェックできる。

生ポインタの取得と再初期化

生ポインタの取得はunique_ptrと同様getメソッドでshared_ptrが管理している領域の生ポインタを取得出来る。当然、このメモリ領域はdeleteしてはならない。同様に、resetメソッドで別のポインタで再初期化できる。unique_ptrとの違いは参照回数が減るだけで、別の同一のメモリ領域を参照しているshared_ptrそのものには影響が無い。

int _tmain(int argc, _TCHAR* argv[])
{
  std::shared_ptr<CPrimitiveCircle> c1(new CPrimitiveCircle(10, 20, 100));

  // 振る舞いはあくまでもポインタ
  std::cout << "count = " << c1.use_count() << " Addr = " << c1.get() << std::endl;

  std::shared_ptr<CPrimitiveCircle> c2 = c1; // 参照回数が増える
  std::cout << "count = " << c1.use_count() << " Addr = " << c1.get() << std::endl;

  std::shared_ptr<CPrimitiveCircle> c3 = c1; // 参照回数が増える
  std::cout << "count = " << c3.use_count() << " Addr = " << c3.get() << std::endl; // ポインタは同値

  c1.reset(new CPrimitiveCircle(0, 0, 20));  // 別のポインタで初期化
  CPrimitiveCircle* pCircle = c1.get();
  std::cout << "count = " << c1.use_count() << " Addr = " << c1.get() << std::endl;
  std::cout << "count = " << c3.use_count() << " Addr = " << c3.get() << std::endl; // 再初期化した分参照回数が減る

  return 0;
}

以下は実行結果。

count = 1 Addr = 505400
count = 2 Addr = 505400
count = 3 Addr = 505400
count = 1 Addr = 505470
count = 2 Addr = 505400  ← resetしたので、参照回数が減っている

operator boolでshaed_ptrが有効か否かのチェック、swapメソッドでポインタの交換が出来るのもunique_ptrと同様。

shared_ptrのちょっとした問題

shared_ptrは参照数で管理しているので、データ構造によっては直接的、間接的にshared_ptr同士が参照し合う現象(相互参照)が発生してメモリ領域が開放されない場合がある。以下の例は、多数の線分の接続関係からポリゴンを作成するロジックから抜粋したもの。

// 直線クラス
class CPrimitiveSegment
{
public:
  CPrimitiveSegment() :
    sx(0), sy(0), ex(0), ey(0)
  {
    std::cout << "CPrimitiveSegment::CPrimitiveSegment()" << std::endl;
  }
  ~CPrimitiveSegment()
  {
    std::cout << "CPrimitiveSegment::~CPrimitiveSegment()" << std::endl;
  }

  std::shared_ptr<CPrimitiveSegment> next; // 接続先
  int sx, sy, ex, ey;
};

int _tmain(int argc, _TCHAR* argv[])
{
  // 線分を3本定義
  std::shared_ptr<CPrimitiveSegment> s1(new CPrimitiveSegment());
  std::shared_ptr<CPrimitiveSegment> s2(new CPrimitiveSegment());
  std::shared_ptr<CPrimitiveSegment> s3(new CPrimitiveSegment());

 // 三角形を作る
  s1->next = s2;
  s2->next = s3;
  s3->next = s1;

  // shared_ptrの参照数を出力
  std::cout << "count s1 = " << s1.use_count() << std::endl;
  std::cout << "count s2 = " << s2.use_count() << std::endl;
  std::cout << "count s3 = " << s3.use_count() << std::endl;

  return 0;
}

実行結果

CPrimitiveSegment::CPrimitiveSegment()
CPrimitiveSegment::CPrimitiveSegment()
CPrimitiveSegment::CPrimitiveSegment()
count s1 = 2
count s2 = 2
count s3 = 2

参照回数が2のまま、main関数が終了しデストラクタが実行されない。いわゆる「メモリリーク」が発生する。

この場合、参照関係を一方通行にして、参照される側の参照カウンタを「変化させない」weak_ptrを使用する。

class CPrimitiveSegment
{
public:
  CPrimitiveSegment() :
	sx(0), sy(0), ex(0), ey(0)
  {
    std::cout << "CPrimitiveSegment::CPrimitiveSegment()" << std::endl;
  }
  ~CPrimitiveSegment()
  {
    std::cout << "CPrimitiveSegment::~CPrimitiveSegment()" << std::endl;
  }
  // shared_ptrから参照数を変化させないweak_ptrへ
  std::weak_ptr<CPrimitiveSegment> next; // 接続先
  int sx, sy, ex, ey;
};

int _tmain(int argc, _TCHAR* argv[])
{
  // 線分を3本定義
  std::shared_ptr<CPrimitiveSegment> s1(new CPrimitiveSegment());
  std::shared_ptr<CPrimitiveSegment> s2(new CPrimitiveSegment());
  std::shared_ptr<CPrimitiveSegment> s3(new CPrimitiveSegment());

 // 三角形を作る
  s1->next = s2;
  s2->next = s3;
  s3->next = s1;

  // shared_ptrの参照数を出力
  std::cout << "count s1 = " << s1.use_count() << std::endl;
  std::cout << "count s2 = " << s2.use_count() << std::endl;
  std::cout << "count s3 = " << s3.use_count() << std::endl;

  return 0;
}

以下は実行結果。参照回数は変化せずにデストラクタが正しく実行されている。

CPrimitiveSegment::CPrimitiveSegment()
CPrimitiveSegment::CPrimitiveSegment()
CPrimitiveSegment::CPrimitiveSegment()
count s1 = 1
count s2 = 1
count s3 = 1
CPrimitiveSegment::~CPrimitiveSegment()
CPrimitiveSegment::~CPrimitiveSegment()
CPrimitiveSegment::~CPrimitiveSegment()