Home > C++ > Pimpl イディオムのお手軽な実装

Pimpl イディオムのお手軽な実装

  • Posted by: zakio
  • 2009年4月15日 22:21
  • C++

Pimpl イディオム(Handle-Body, Compilation or Compiler Firewall, Cheshire Cat とも呼ばれたりする)は実装の詳細を隠蔽したい時に用いられる手法だが、内部が委譲の嵐になったり、ポインタの扱いに注意を払う必要があったりと、気軽には使い難いパターンの一つではある。内部の委譲に関しては Pimpl の性質上仕方がないものとして諦められるが、せめてポインタ周りだけでもお手軽に扱えるようにならないだろうか、というのが今回のお題だ。

教科書に載っているような Pimpl の実装は、大抵の場合、次の例の前半部分まで。こうやって隠蔽した Impl のポインタ pImpl_ を正しく扱うためには、コンストラクタ、デストラクタ、コピーコンストラクタ、代入演算子を「正しく」実装しなければならない。


class PimplTest {
public:
  // ... 外部に公開するインタフェース

private:
  class Impl; // 実装の詳細は Impl に隠蔽し、
  Impl* pImpl_; // そのポインタを保持する

// Impl を正しく扱うためには、以下のコードを「正しく」書かなければならない
public:
  PimplTest(); // ここで new
  ~PimplTest(); // ここで delete
  PimplTest(const PimplTest& obj); // Impl に対する深いコピーが必要
  PimplTest& operator = (const PimplTest& rhs); // こちらも同様に深いコピー
};

時々、ポインタ周りを楽にするために shared_ptr を使って Pimpl イディオムを実装している例を見かけるが、これではコピーや代入時に実装の詳細が共有されてしまう。コピーや代入を禁止しても構わない場合ならこれで充分かも知れないが、そうでない場合は自前のコピーコンストラクタや代入演算子を用意する必要があるため、やはり、お手軽とは言い難いだろう。

例えば、先ほどの例を少しだけ肉付けして、shared_ptr を使った例に書き換えると次のようになる。


class PimplTest {
public:
  PimplTest();

// 値の読み書きを表現
public:
  void SetVal(int val);
  int GetVal() const;

// 実装の詳細
private:
  class Impl;
  std::tr1::shared_ptr<Impl> pImpl_; // コピーさえしなければ、これで間に合うが......
};

cpp 側はこんな感じ。


// 実装の詳細
class PimplTest::Impl {
  int val_;
public:
  Impl(): val_(int()) {}
  void SetVal(int val) { val_ = val; }
  int GetVal() const { return val_; }
};

// コンストラクタで new するだけなので楽チン
PimplTest::PimplTest(): pImpl_(new Impl) {}

// 委譲の嵐
void PimplTest::SetVal(int val) { pImpl_->SetVal(val); }
int PimplTest::GetVal() const { return pImpl_->GetVal(); }

このように実装してしまうと、shared_ptr が持つ「コピー時に参照先(この場合は Impl のインスタンス)を共有する」という性質から、次のような問題が生じてしまう。


// PimplTest のインスタンスに 1 と 2 をセットする
PimplTest test1, test2;
test1.SetVal(1);
test2.SetVal(2);

// 当然、1, 2 と表示される
cout << test1.GetVal() << ", " << test2.GetVal() << endl;

test1 = test2; // test2 の内容を test1 に代入(これで test1 が 2 になるはず)
test2.SetVal(20); // test2 は 20 に変更

// 2, 20 と表示されることを期待するが、
// 実際には test1 と test2 は同じ Impl を参照しているため、20, 20 と表示される
cout << test1.GetVal() << ", " << test2.GetVal() << endl;

つまり、Pimpl に適したスマートポインタは、次の2つの機能を兼ね備えていなければならないということだ。

  1. 不完全型を受け付ける(クラスの先行宣言だけでコンパイル可能)
  2. 値のセマンティクスを持つ(勝手に深いコピーをしてくれる)

shared_ptr は1つ目の機能を deleter によって実現しているが、残念ながら2番目の機能が期待通りではない。どこかに上記の機能を兼ね備えた Pimpl に最適なスマートポインタはないかと探し回ったが、探し方が悪いのだろう。満足のいくものを見つけることは出来なかった。そこで、勉強も兼ねて自作に踏み切ってみることにした。車輪の再発明かも知れないが、結構勉強になったので私的には大満足だ。

気に入った方は好きに使っていただいて構わないし、改変や商用利用、その他諸々何でもアリ。ただし、当然ながら無保証なのでご注意を。

この特別な Pimpl イディオム用のスマートポインタ Pimpl を使えば、先ほどのコードが次のように変化し、


class PimplTest {
public:
  PimplTest();

public:
  void SetVal(int val);
  int GetVal() const;

private:
  class Impl;
  Pimpl<Impl> pImpl_; // この部分を Pimpl に変更
};

実行結果も期待通りになる。


// PimplTest のインスタンスに 1 と 2 をセットする
PimplTest test1, test2;
test1.SetVal(1);
test2.SetVal(2);

// 当然、1, 2 と表示される
cout << test1.GetVal() << ", " << test2.GetVal() << endl;

test1 = test2; // test2 の内容を test1 に代入して、
test2.SetVal(20); // test2 は 20 に変更

// 今度は期待通り 2, 20 と表示される
cout << test1.GetVal() << ", " << test2.GetVal() << endl;

Pimpl の実装は以下の通り。これを適当な namespace に入れてインクルードガードを付ければ Pimpl.h の完成だ。結構便利に使えると思うのだが如何だろう。


// 不完全型を受け付けるためのインタフェース
// ポインタを void* で扱っている部分がミソ
class ImplHolderBase {
public:
  virtual ~ImplHolderBase() {}
  virtual ImplHolderBase* Clone() const = 0;
  virtual void CopyTo(ImplHolderBase* pHolder) const = 0;
  virtual void* GetPtr() = 0;
  virtual const void* GetPtr() const = 0;
};

// ポインタを保持するクラス
template <class T>
class ImplHolder: public ImplHolderBase {
  T* ptr_; // これが Impl の実体
public:
  explicit ImplHolder(T* ptr): ptr_(ptr) {}
  virtual ~ImplHolder() {
      delete ptr_;
  }
  virtual ImplHolderBase* Clone() const {
      return new ImplHolder(new T(*ptr_)); // ここで T のコピーコンストラクタが呼ばれる
  }
  virtual void CopyTo(ImplHolderBase* pHolder) const {
      // ImplHolder 以外は考えられないので、static_cast で変換可能
      *(static_cast<ImplHolder*>(pHolder)->ptr_) = *ptr_; // ここで T の代入演算子が呼ばれる
  }
  virtual void* GetPtr() { return ptr_; }
  virtual const void* GetPtr() const { return ptr_; }
private: // 念のためコピーと代入を禁止しておく
  ImplHolder(const ImplHolder&); // 未実装
  ImplHolder& operator = (const ImplHolder&); // 未実装
};

// Pimpl クラスの実体
template <class T>
class Pimpl {
  ImplHolderBase* pHolder_; // T に依存しない
public:
  explicit Pimpl(T* ptr): pHolder_(new ImplHolder<T>(ptr)) {} // この時点で T の型が決まる
  ~Pimpl() {
      delete pHolder_; // T が不完全型でも問題ない
  }
  Pimpl(const Pimpl& init): pHolder_(init.pHolder_->Clone()) {} // コピーコンストラクタは Clone で対応
  Pimpl& operator = (const Pimpl& rhs) {
      rhs.pHolder_->CopyTo(pHolder_); // 代入は CopyTo で対応
      return *this;
  }
  T& operator * () { return *(static_cast<T*>(pHolder_->GetPtr())); }
  const T& operator * () const { return *(static_cast<const T*>(pHolder_->GetPtr())); }
  T* operator -> () { return &(**this); }
  const T* operator -> () const { return &(**this); }
};

ぜひ、お試しあれ!

Comments:0

コメントを表示する前に、このブログのオーナーによる承認が必要になることがあります。コメントが表示されない場合は、承認されるまでしばらくお待ちください。

Comment Form

Home > C++ > Pimpl イディオムのお手軽な実装

Search
Feeds

Return to page top