pImpl + thin template 技法の簡単な例を作ってみた。

テンプレート使うとヘッダが肥大化してコンパイル&リンク時間が爆発する! なんとかして!
という問題への対処法です。

コードはこちら:
http://gist.github.com/640298


以下、解説…ですが、しっかりした解説を書くのは面倒なので、箇条書き程度に。

  • 基本アイデアとしては、実装の詳細は非テンプレートの実装用クラスの内部クラスに隔離し、テンプレートはその薄いラッパにすることによって、ヘッダから実装を取り除く、というものです(thin template 技法)。
  • 通常の thin template では、非テンプレートの実装用クラスのメンバ構成までは秘匿できませんが、そこを pImpl にすることで、ほぼ完全に実装を秘匿できます。
  • ただし一点、メモリ管理だけは秘匿しにくいので、そこは boost::shared_ptr を使って「型を消す」ことで対処しています( shared_ptr は生の void* と違い、確実に正しい破棄関数を呼んでくれます)。
  • shared_ptr の代わりに、 virtual なデストラクタを持った共通基底クラスを使っても対処できます。または、ラッパ側のテンプレートで new と delete を明示的に呼び出すことでも対処できます(が、うっかりミスをしやすい設計なので、オススメはしないです)。
  • 今回は内部実装を露出させない形で作りましたが、格納された shared_ptr を取得できるようなインターフェイスにしても面白いと思います。ただ、その場合は設計をよく練る必要がありそうです( const はどうするか、コピー時の動作はどうするか、など)。
  • 実装クラスの pImpl において、一般的な pImpl のように生ポインタを使わず、代わりに boost::scoped_ptr を使っているのは、デストラクタの書き忘れによるうっかりミスを避けるため。 scoped_ptr を使えば、デストラクタを書き忘れるとコンパイルエラーになってくれます。
  • scoped_ptr にはコピーを防止する意味もあります。生ポインタの場合はコピーすると大変なことになりますが、 scoped_ptr を使った時点で noncopyable なので、明示的に boost::noncopyable を継承せずとも大丈夫。
  • scoped_ptr は便利ですが、厳密には標準ではないので、 C++0x が使えるようなら標準の std::unique_ptr を使ったほうがいいかもしれません。その場合は move-ctor を明示的に定義する(か削除する)必要があります。
  • 見て分かるとおり、 pImpl にする分と thin template にする分とで、確実に実行時のコストは増大しています。が、下手に最適化しようとすると、厄介な問題を引き起こすので、そこまでするならやらない方がいいです。
  • というか、下手に最適化するくらいなら、遠慮無くヘッダに実装を書き、翻訳単位をそもそも一つしか用意しないようにする方法の方が優れてる場合がほとんどです。少なくとも実行時のコストを考えるなら、最適化も効きやすいですし、それがベストです。リンク時間を節約することにもつながります。