おぺんcv

画像処理エンジニアのブログ

継承を用いたPimplのようなこと

はじめに

C++における実装の隠蔽やインクルード依存削減についての話です
おそらく過去に何度も議論されていて何番煎じのネタかわかりませんが、自分にとっての気づきだったので書いておきます

Pimplイディオムといえば…

クラスに「Impl」という実装担当クラスのポインタを保持し、諸々の処理をImplクラスに移譲することにより、

  • 内部実装の隠蔽
  • ヘッダ側への余計なインクルード回避

を実現するテクニックとして知られています

////////////////////////////////////////////////
// Hoge.h
////////////////////////////////////////////////
class Hoge
{
public:
  Hoge();
  ~Hoge();
  void method1();
  void method2();
private:
  class Impl;
  std::unique_ptr<Impl> impl_;
};

////////////////////////////////////////////////
// Hoge.cpp
////////////////////////////////////////////////

// 実装クラス
class Hoge::Impl
{
  void method1()
  {
    ...
  }

  void method2()
  {
    ...
  }
};

Hoge::Hoge()
{
  impl_ = std::make_unique<Hoge::Impl>();
}

Hoge::~Hoge()
{
}

//  Hoge::Implに処理を移譲
void Hoge::method1()
{
  impl_->method1();
}

void Hoge::method2()
{
  impl_->method2();
}

この方法を知った当初は「実装隠蔽だなんてそんな潔癖にならなくても…」と思ってたのですが、最近はようやく意義をわかりかけてきました

クラスの規模が開発につれ段々と大きくなってくると、メンバ変数を増やしたり、それに伴う新たなインクルードを追加する必要が出てくると思いますが、それはつまりヘッダを修正することになるので、そのヘッダをインクルードしているほかのファイルも再コンパイルしなければなりません

Pimplを使えばヘッダ側を修正しなくて済むのでこのような問題を回避できますね!

ただし、面倒な点があるとすれば、上記のようなImplへのたらい回しコードをmethodが増えるたびに書かなければいけないことです

継承を用いた方法

これもPimplと同じ効果が得られ、アリかな思う方法です

////////////////////////////////////////////////
// Hoge.h
////////////////////////////////////////////////
class Hoge
{
public:
  std::unique_ptr<Hoge> create();
  virtual void method1() = 0;
  virtual void method2() = 0;
};

////////////////////////////////////////////////
// Hoge.cpp
////////////////////////////////////////////////

class HogeImpl : public Hoge
{
  void method1() override
  {
    ...
  }

  void method2() override
  {
    ...
  }
};

std::unique_ptr<Hoge> Hoge::create()
{
  return std::make_unique<HogeImpl>();
}

Hogeは抽象クラスになっているのでインスタンス化はできませんが、ユーザはcreate()を通じてHogeのポインタを受け取ることができます

このときcreate()側ではHogeを継承したHogeImplのポインタを返してしまおうという手です
メリットとしては

  • たらい回しコードを書かなくてよい
  • ヘッダ側にImplを宣言する必要もない

といったところでしょうか、一方デメリットとしては

  • 本来の継承の使い方ではない(is-a関係を表すものではない)
  • 仮想関数を使用することによるコスト
    • まあクリティカルな部分でこんな方法使わないと思うけど…

などが考えられます

おわりに

継承を使ったやり方を知ったのはOpenCVを使ってたのがきっかけで、OpenCVのクラスにはインスタンスをポインタ経由でしか生成できないものがあります
例えばこんな感じです

cv::Ptr<cv::StereoSGBM> sgbm = cv::StereoSGBM::create();
sgbm->compute(...);

このクラスのヘッダを見るとpublic関数の宣言だけで実装に関する記述が無く、実装を見ると前述のコードのようになっていて「これってPimplと似てるな~」と思った次第です

お断りしておくと、OpenCVのStereoSGBMクラスはStereoMatcherという抽象的なステレオマッチングクラスを継承しており、create()はFactory Method的に使っているのであって、本来の継承の使い方をしています

以上、Pimplについて思うことを書いてみました