pimpl, scoped_ptr

c++で、あるクラスAの実装とクラスAの利用者との間の依存関係を断ち切る為に使われるテクニックに、pimplイディオムというのがあるらしい。おぉ!これは便利、と思って、じゃぁ使ってみましょうか、ということでちょっと調べた。

そもそもpimplイディオムとは
A.h

class A
{
public:
  A();
  ~A();
  void doSomething();
private:
  class Impl;
  Impl* impl;
};

A.cpp

#include "A.h"

class A::Impl
{
public:
  Impl()
  {
    // construct impl
  }

  ~Impl()
  {
    // destruct impl
  }

  void doSomething()
  {
    // do something
  }
};

A::A() : impl(new Impl()) {}

A::~A() 
{
  delete impl;
}

void A::doSomething()
{
  impl->doSomething();
}

みたいな感じで、実装を内部クラスに丸々委譲してしまうもの。こうすると、実装に関する情報が全部A.cppの中にまとまっていて、Aの実装を変更した時に、その変更の影響がA.hをインクルードしているクラスAの利用コードに及ばない、というもの。

まず、生ポインタを嫌ってboost::shared_ptrを使うときの注意。shared_ptrだと、クラスAのコピーコンストラクタとoperator=をきちんとディープコピーにしてあげないと(暗黙に作成されるものを使っていると)、中身のデータがクラスAの複数のインスタンスで共有されちゃうよ、と。まぁ、これはでもポインタ使って居る以上当然想像されるべき振る舞い。

で、これに起因するうっかりミスを防ぐためにコピーコンストラクタとoperator=をprivateに隠したscoped_ptrというのがある。「scoped_ptrはpimplイディオムの実装によく使われます」と説明している人も居るぐらいなので、まぁpimplにはこのスマポなんだろうと理解していた。ところが気楽に使っていると?なコンパイルエラーが出る。以下、その例。scoped_ptrもスマートポインタなので、ポインタの解放はお任せしたい。で、こんなコードを書く。

B.h

#include <boost/scoped_ptr.hpp>

class B
{
public:
  B();
  void doSomething();
private:
  class Impl;
  boost::scoped_ptr<Impl> impl;
};

B.cpp

#include "B.h"

using namespace boost;

class B::Impl
{
public:
  Impl()
  {
    // construct impl
  }

  ~Impl()
  {
    // destruct impl
  }

  void doSomething()
  {
    // do something
  }
};

B::B() : impl(new Impl()) {}

void B::doSomething()
{
  impl->doSomething();
}

これは、B.cppのコンパイルは通るけど、別のファイルで

#include "B.h"
...
B b;
...

とした時点でコンパイルエラーが発生する。これを正しく使うためには、
B.hで

~B();

とデストラクタの宣言だけをしておいて
B.cppで、クラスB::Impleの定義よりも後に

B::~B(){}

とデストラクタの定義を書かなければならない。何でこんなことになるかっていうと、B.hにデストラクタの宣言が無い時のコンパイラの振舞に理由がある。B.hにデストラクタの宣言が無いと、コンパイラは暗黙的に、何もしないデストラクタがB.hにあるものと見なす。するとすると、この何もしないデストラクタの中では、implの破棄が行われるが、implはscoped_ptr型だから破棄される際にはImpl*の解放という作業が発生する。しかししかし、B::Impl型の詳細はB.cppの中に隠れちゃってるんだから、B.hをインクルードしてるだけの立場からすると、こいつは不完全型である。で、不完全型を指すポインタに対するdeleteは動作未定義で問題有りなので、コンパイルエラーが発生する、ということだそうだ。ちなみにコンパイルエラーを出してくれるのはscoped_ptrだからで、生ポインタを使っていると、「ここん所で不完全型を指すポインタを解放する時にはそのクラスのデストラクタは呼ばれないよ」と親切に警告だけ出してくれてエラーにはならない。うーん。恐ろしい。

c++難しすぎるよ。