スマートポインタのつくりかた

2年ちょっと前にC++で書いた非侵入的参照カウント方式のスマートポインタ実装を発掘した. スマートポインタの実装にはC++の様々なマニアックな実装手法が隠れていて面白い. せっかくなので, 解説つきで晒すことにした.

このスマートポインタはboost::shared_ptr (以下単にshared_ptr)相当のもので, 当時C++のコードを書かないといけない状況下で「boost使っちゃらめぇぇぇ!」とか言われたために, せめてshared_ptrだけでも抜き出して使おうとしたものの, 依存しているファイルの数が思ったより多くて面倒になったので, 1ファイルで完結する簡易実装を自分で書くことにした, という経緯で生まれたもの. 似たような状況では有用かも知れないし, shared_ptrのソースコードを読む助けにはなるかも知れないけれど, 基本的にはboostが使えるなら素直にshared_ptrを使うべき.

「非侵入的」については詳しく書かない. 参照カウント方式やそれ以外の方式についても含めて, スマートポインタの種類と動作について知りたければ, たとえば以下の参考文献を見ると詳しく説明されている.

今回のスマートポインタ実装はshared_ptrと区別するためにsmart_ptrという名前にしておく*1. 完成版ソースコードは一番下にある.

shared_ptrとsmart_ptrの差異

shared_ptrをコピペして作ったわけではないので, smart_ptrの細かい実装手法はshared_ptrのそれとは異なる部分もある. それでも基本的な機能や実装の細部のうち本質的な部分はほぼ同じと思っていい. ただし, shared_ptrのうちのいくつかの重要な機能や最適化はsmart_ptrでは省かれている. 省かれた主な特徴は以下の通り.

  • 非侵入的参照カウント方式以外のスマートポインタとの相互運用
  • スレッドセーフ
  • 右辺値参照(C++0xの新機能)を使った最適化(move semantics)への対応
  • 例外処理
  • ポインタ値の大小比較
  • ストリームへの出力

逆に言えば, とくに最初の3つは簡易版でやるには手に余る, なかなか真似できない価値ある実装になっているので, 是非boostのソースコードを読んでみよう. 最後の3つがsmart_ptrに実装されていないのは単なる怠惰. 読者への宿題.

スマートポインタとは

スマートポインタが何なのかを見る前に, スマートでないポインタクラスを見てみよう. 以下のpoor_auto_ptrは, newされたオブジェクトを自動的にdeleteするためのもので, コンストラクタでオブジェクトのポインタを受け取って, デストラクタでdeleteする.

template <class T>
struct poor_auto_ptr {
  poor_auto_ptr(T* p):p_(p){}
  ~poor_auto_ptr(){ delete p_; }
private:
  T* p_;
};
class A { /* ... */ };

int main(void) {
  poor_auto_ptr<A> a1(new A(1));
  if (1) {
    poor_auto_ptr<A> a2(new A(2));
    if (2) {
      poor_auto_ptr<A> a3(new A(3));
    } // delete [A(3)]
  } // delete [A(2)]
  return 0;
} // delete [A(1)]

オブジェクトの寿命がpoor_auto_ptrのインスタンスのスコープと対応していれば, これでも全く問題ない. ところが, そうでない場合は困ったことになる.

int main(void) {
  poor_auto_ptr<A> a1(new A(1));
  if (1) {
    poor_auto_ptr<A> a2(new A(2));
    if (2) {
      poor_auto_ptr<A> a3(new A(3));
      a1 = a3; // (*)
    } // delete [A(3)] (!)
  } // delete [A(2)]
  return 0;
} // delete [A(3)] (!!)

if (2)でポインタa1にa3を代入すると, a1とa3は同じオブジェクトA(3)を指している. このとき, 2つの問題が発生する.

  1. A(1)がdeleteされない
  2. A(3)を2回deleteしてしまう

1つ目は(*)でa1が元々指していたA(1)をdeleteしていないことが原因. 2つ目は, a1とa3が最終的に同じオブジェクトA(3)を指しているために, 既にdeleteしたにもかかわらず(!!)でもう一度deleteしてしまうのが原因. 言い換えれば, (!)ではまだa3の指すオブジェクトはより寿命の長いa1にも参照されているにもかかわらずdeleteしてしまっているのが原因とも言える.

スマートポインタは, ポインタが寿命を終えるとき, 参照しているオブジェクトが他のどのポインタからも参照されていないときだけdeleteする.

int main(void) {
  smart_ptr<A> a1(new A(1));
  if (1) {
    smart_ptr<A> a2(new A(2));
    if (2) {
      smart_ptr<A> a3(new A(3));
      a1 = a3; // delete [A(1)]
    } // deleteしない
  } // delete [A(2)]
  return 0;
} // delete [A(3)]

今回の実装では, 参照カウント方式によってこれを実現する. 参照カウント方式では, 参照されているオブジェクトとともに, 参照しているポインタの数を記録していき, 参照カウントが0になったときだけdeleteする. 上の例に参照カウントを書き加えてみると以下のようになる.

int main(void) {
  smart_ptr<A> a1(new A(1)); // [A(1):1]
  if (1) {
    smart_ptr<A> a2(new A(2)); // [A(2):1]
    if (2) {
      smart_ptr<A> a3(new A(3)); // [A(3):1]
      a1 = a3; // [A(1):0] => delete [A(1)]
               // [A(3):2]
    } // [A(3):1]
  } // [A(2):0] => delete [A(2)]
  return 0;
} // [A(3):0] => delete [A(3)]

参照カウンタ

まずは参照カウンタの実装を見てみよう. 参照カウンタは, オブジェクトを参照しているポインタの個数を保持するために使用される. ポインタの数が増えても参照カウンタそのものは増えず, 参照しているポインタがなくなった時点で参照カウンタ自身も消滅する.

以下のcounted_refクラス(boost::detail::shared_count相当)は参照カウンタを操作するためのクラスで, そのインスタンスはsmart_ptrのインスタンスと一対一に結びつけられて動作する.

struct counted_ref {
  counted_ref(void):pn_(0){}
  counted_ref(const counted_ref& r):pn_(r.pn_){ if (pn_) pn_->inc(); }
  template<class S, class D>
  explicit counted_ref(S* p, D d):pn_(new impl(p,d)){}
  ~counted_ref(void){ release(); }
  void release(void) {
    if (pn_ && !pn_->dec()) {
      pn_->d_->dispose();
      delete pn_;
    }
  }
  counted_ref& operator=(const counted_ref& r) {
    impl* tmp = r.pn_;
    if (tmp != pn_) {
      if (tmp) tmp->inc();
      release();
      pn_ = tmp;
    }
    return *this;
  }
private:
  struct impl {
    template<class S, class D> explicit impl(S* p, D)
      :n_(1),d_(D::template type<S>::name::get(p)){}
    impl(const impl& c):n_(c.n_),d_(c.d_){}
    size_t inc(void){ return ++n_; }
    size_t dec(void){ return --n_; }
    ~impl(void){ d_->destroy(); }
    size_t n_;
    deallocator_base* d_;
  };
  impl*  pn_;
};
コンストラク

counted_refのデフォルトでもコピーでもないコンストラクタは, まだ誰にも参照されていないオブジェクトのために新しい参照カウンタ本体(counted_ref::implクラスのインスタンス)を生成して, そのポインタをpn_メンバに保持する.

コンストラクタの第2引数は, 参照しているオブジェクトを削除するときに, 削除の仕方を知っているクラスを表すタグになっている(タグなので, よく見るとcounted_ref::implはこの値を使っておらず, 型情報を伝播するための引数だとわかる). これは, 単にdeleteすればいいのか, delete[]しなければいけないのか, はたまた全く別の方法によるのか, 削除方法の詳細をcounted_refは関知しないようにするため.

コピーコンストラク

参照カウンタ本体は同じオブジェクトに対しては1つだけで, counted_refのコピーコンストラクタでpn_のポインタ値をコピーすることで, 同じオブジェクトを参照しているcounted_refインスタンスは参照カウンタを共有する.

参照の解除(release関数)

counted_refのデストラクタが呼ばれたり, 別のオブジェクトを参照しようとするとき(operator=)に参照カウントを減らし, 参照カウントが0になったら参照カウンタ本体をdeleteし, 参照先のオブジェクトも削除する. 実際にオブジェクトを削除する処理は, 参照カウンタに覚えさせておいたdeallocator_baseクラス(後述)のdispose関数に任せる.

deallocator

deallocator_baseクラスは単なるインタフェースで, virtual関数disposeとdestroyを持つ. 実際にオブジェクトを削除するのは, これを継承したクラス. 以下は, deleteでオブジェクトを削除するシンプルなdeallocatorの例.

template <class T>
struct simple_deallocator : public deallocator_base {
  typedef simple_deallocator<T> this_type;
  virtual void dispose(void){ delete p_; }
  virtual void destroy(void){ delete this; } // it knows itself how to die
  static this_type* get(T* p){ return new this_type(p); } // factory pattern
private:
  this_type& operator=(const this_type&){ return *this; }
protected:
  simple_deallocator(const this_type& rhs):p_(rhs.p_){} //forbidden
  simple_deallocator(void):p_(0){} // forbidden
  simple_deallocator(T* p):p_(p){} // forbidden from outisde
  T* p_;
};

deallocatorは, 自分自身のメモリ領域も自分自身で削除しなければならない. そのため, destroyメソッドはdelete thisする. これはC++の黒魔術の中でもかなり危険なものなので, せめて誤用を避けるためにget関数内のnew以外ではインスタンスを生成できないようにしておく.

smart_ptr

これだけ用意しておけば, smart_ptrはこれらを使うだけ. ほぼ何も難しいところはない.

template<class T, class RefObject=counted_ref> struct smart_ptr {
  // default constructor
  smart_ptr(void):px_(0),r_(){}
  // copy constructors
  smart_ptr(const smart_ptr<T, RefObject>& s):px_(s.px_),r_(s.r_){}
  template<class S>
  smart_ptr(const smart_ptr<S, RefObject>& s):px_(s.px_),r_(s.r_){}
  // constructors from a raw pointer
  template<class S>
  explicit smart_ptr(S* p):px_(p),r_(p,simple_dealloc()){}
  template<class S, class D>
  explicit smart_ptr(S* p, D d):px_(p),r_(p,d){}
  // destructor
  ~smart_ptr(void){}
  // references
  T& operator*()const{ return *get(); }
  T* operator->()const{ return get(); }
  T* get(void)const{ return px_; }
private:
    template<class S, class R> friend struct smart_ptr;
    T* px_;
    RefObject r_;
};
サブタイプのポインタを受け付ける

スマートポインタはできる限りふつうのポインタとして振る舞わせたい. たとえば,

class A { /* ... */ };
class B : public A { /* ... */ };

int main(void) {
  smart_ptr<A> a(new B());
  return 0;
}

のように, Aのスマートポインタ型の変数aにBクラスのオブジェクトを格納できるようにしたい. これを実現するために, smart_ptrのコンストラクタの一部はテンプレート引数を取るようになっている.

ところが, 素朴にテンプレートにしただけでは少し困ったことが起きる. Aクラスのデストラクタは非virtualなので,

A* a = new B();
delete a;

のようにしたとき, C++では最後のdeleteの挙動は未定義とされている*2. 一つ前の例でもこの状況が起きてしまうのではないか? この問題に対処するため, たとえスーパタイプのポインタが最後の参照だったとしても, deallocatorは本来の型のポインタをdeleteするようになっている.

これを達成するのが, counted_ref::implのコンストラクタでdeallocatorを初期化している部分と, deallocatorのタグsimple_deallocの定義. まず, counted_ref::implのコンストラクタでは, ポインタの実際の型Sをテンプレート実引数として指定することで, タグから生成すべきdeallocatorの型を求めている.

class counted_ref {
  /* ... */
private:
  struct impl {
    template<class S, class D> explicit impl(S* p, D)
      :n_(1),d_(D::template type<S>::name::get(p)){}
    /* ... */
  };
  impl*  pn_;
};

simple_deallocの定義は

struct simple_dealloc {
  template<class T> struct type : public detail::simple_deallocator<T> {
    typedef detail::simple_deallocator<T> name;
  };
};

のようになっている. counted_ref::implのテンプレート引数Dには, smart_ptrのコンストラクタで渡された型名simple_deallocが渡ってくる. すると, simple_dealloc::type<S>::nameはsimple_deallocator<S>型になるので, simple_deallocator<S>::getによってsimple_deallocator<S>のインスタンスがnewされる(ファクトリメソッドパターンのテンプレートを用いた応用). newされたdeallocatorにはsmart_ptrのテンプレート引数に渡された型Sの情報が残る. counted_ref::implはこのdeallocatorをインタフェースdeallocator_base型で保持しておくので, 以降はdeallocator自身を除いて型Sのことは忘れる(いわゆる型消去).

ちなみに, simple_deallocの定義がクラス(構造体)の入れ子になっているのは, C++にtemplate typedefが無いためで, template typedefが欲しくなったときにはこのようにするのが定石なので, イディオムとして覚えておくとよいかも知れない.

追記

2010-07-25T15:08:12+0000
スマートポインタの概要を加筆

*1:名前空間は分けてあるから衝突する心配はないにもかかわらず, 実は2年前の時点でshared_ptrではなくsmart_ptrという名前にしてあったけれど, どうして別の名前にしたのかは忘れた

*2:ISO/IEC 14882:2003(E), p83, 5.3.5-3