[C++] 実装の隠蔽:Pimplイディオム

2020 年 6 月 2 日

ヘッダファイルの依存関係を減らしたり、実装を隠蔽したりするための技法のひとつ。

コピー禁止の場合

ヘッダファイル側(Sample.h )
[cpp]
class Sample {
public:
Sample(const int num);
~Sample();

void getNum() const;

private:
class Impl; // 実装用クラスの前方宣言
Impl* impl;

// コピー禁止
Sample(const Sample&);
void operator=(const Sample&);
};
[/cpp]

実装ファイル側(Sample.cpp)
[cpp]
// 実装用の内部クラスの定義・実装
class Sample::Impl {
private:
int num;

public:
Impl(const int num) : num(num) { };

int getNum() const { return num; }
};

// Sampleクラスの実装
Sample::Sample(const int num) : impl( new Impl(num) ) {

}

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

int Sample::getNum() const {
return impl->getNum(); // 委譲
}
[/cpp]

コピー可能の場合

ヘッダファイル側(Sample.h )
[cpp]
class Sample {
public:
Sample(const int num);
Sample();
Sample(const Sample&);
~Sample();

Sample& operator=(const Sample&);

int getNum() const;

private:
class Impl; // 実装用クラスの前方宣言
Impl* impl;
};
[/cpp]

実装ファイル側(Sample.cpp)

[cpp]
class Sample::Impl {
private:
int num;

public:
Impl(const int num) : num(num) { };
Impl() : num(1) { }

int getNum() const { return num; }
};

Sample::Sample(const int num) : impl( new Impl(num) ) {

}

Sample::Sample() : impl(new Impl) {

}

Sample::Sample(const Sample& base) {
// Implの新しいインスタンスをコピーコンストラクタを使って生成
Impl* newObj = new Impl(*base.impl);
impl = newObj;
}

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

Sample& Sample::operator=(const Sample& base) {
// コピーコンストラクタの場合と同様、implを新しく生成
Impl* newObj = new Impl(*base.impl);
delete impl;
impl = newObj;

return *this;
}

int Sample::getNum() const {
return impl->getNum(); // 委譲
}
[/cpp]

コピー禁止の場合はともかく、コピー可の場合はスマートポインタを使ったほうが楽。

[cpp]
#include <memory>

std::shared_ptr<impl> impl;

//……………………………..

// implのインスタンスを入れ替える
impl.reset(new Impl(*base.impl));
[/cpp]

レガシー環境の場合:boost::scoped_ptrやboost::shared_ptrを使う。

[cpp]
#include <boost/scoped_ptr.hpp>

boost::scoped_ptr<impl> impl;

//……………………………..

// implのインスタンスを入れ替える
impl.reset(new Impl(*base.impl));
[/cpp]

要点

  • Pimplイディオムといっても、要は実装部分を別クラスにして前方宣言し、そのインスタンスを元々のクラスのメンバとして持ち(包含=コンポジション)、あとはそれぞれのメンバを委譲しているにすぎない。
  • ただし裏を返せば、すべての公開メンバ関数の委譲部分をいちいち記述しなければならない。
  • メンバの一部のみ隠蔽するなど、柔軟な使い方が可能。
  • ファイルサイズが増える。
  • 委譲部分で余計な関数の呼び出しコストがかかる。
  • クラスがコピー可の場合、少々ややこしくなる。
  • クラスの継承時にややこしいことになりやすい。

スマートポインタを使うべき?

基本的に実装クラスのインスタンス(上記のimpl)は、元々のクラスにおける唯一のメンバ変数であり、しかも外部に公開するわけでもないので、スマートポインタを使わなくても素直にデストラクタでdeleteすれば十分だと思われる。

唯一使う価値があるとすれば、クラスをコピー可にする場合くらいだろう。

【理由】

  • せっかくヘッダファイルの依存を減らそうとしているのに、スマポのためのヘッダをincludeしてしまっては本末転倒。
  • スマポで管理するインスタンスのメンバ(変数・関数)にアクセスする際、毎回スマポを通す必要があるので処理のオーバーヘッドがある。
  • Boostの型はテンプレートやtypedefを使いまくっているので、前方宣言が難しい
  • STLのstd::auto_ptrは不完全クラスに使用すると問題が出るので、使わないほうがいい(そもそもC++11で非推奨になった)