Entry

プログラミングメモ - テンプレートでヘッダ肥大化とか

2011年09月17日

C++ でテンプレートを多用していると,必然的にヘッダが肥大化します。で,ヘッダが肥大化すると,コンパイル時,それらが読み込まれる度(include される度 == 通常は翻訳単位ごと)にテンプレートを実体化する処理が走るので,コンパイルが遅くなる問題があります。

もっとも,個人的にはコンパイル時間が長くなるなら待てばいい,とゆもんなので気にしてなかったりします。最近は並列コンパイルも手軽に利用できるので,Many CPU/Multi-Core な環境だったら,ぶちぶち文句をたれるほどのことじゃない。会社で管理しているアプリケーションのソースコードの中には,ほとんどテンプレートで書かれているのもあるけれど,十数万ステップ程度でも数分で終わります(うちの開発環境がやたら豪華だってのもあるんだけれども)。

それよりも,ヘッダが肥大化して困るのは,名前空間の管理が難しくなることです。単品の実行ファイルを作るだけならそれほど問題にならないんだけれども,補助的にライブラリを作る場合(大き目のプロジェクトでは普通のことだが),ライブラリだけを他に使いまわす可能性があることから,名前空間を適切に管理する必要があります。

この点,伝統的な C/C++ のテクニックとして,ライブラリローカルな名前は,オブジェクトコードに隠蔽する(翻訳単位で static な関数/変数を宣言/定義する)ことで管理する名前を限定できたわけだけれども,テンプレートの場合は基本的にすべてが公開されてしまうので,この方法が使えません。変な命名規則を作らなきゃいけなくなる。「そんなもん,namespace を指定してやればいいんでね?」という方もいるでしょうけれど,C で書かれた外部ライブラリは,みんな同じ名前空間に入ってしまうので,利用側がコントロールすることができません(いざとなれば書き換えればいいんだが)。

そんなこんなで,みんながどうしているのかちょっと調べてたんだけれども,pImpl やら Proxy やらで設計すればいいよ,といった話がありました。しかし,これについては,こうすれば絶対に全部オッケーなんて話はないんだと思う。

特に,pImpl は通常ユーザ空間では解決できず,メモリ確保にカーネル(OS)の機能を使わなくちゃいけません。pImpl のポインタ管理にスマートポインタを使うと,そのオーバーヘッドはさらに高くなる。pImpl は言ってみれば,そのクラスにまつわるすべてのオブジェクト生成をヒープ上に行うパターンなので,ホントに本格的にやるなら適切なアロケータも定義する必要があるはずです(オブジェクトの大きさ/使用頻度に応じて適切なアロケーション戦略を採用する)。効果の割にコストが目立ってしまう,と,個人的には思います。しかし,ま,これも選択肢のひとつです。

結局,この話は実体を持ったクラスとテンプレートをどのように接続するか,とゆ話になるわけだけれども,それはライブラリなりアプリケーションなりでどの機能を抽象化してどの名前を隠蔽するかを決めることで決まることなのだと思う。

例えば,テンプレートで書かれた画像クラスに,ファイルを読み込むクラスを作る場合を考えます。テンプレート本来の目的である拡張性を考える場合,読み込むファイルの種類は拡張可能にしておきたい。これに対して,読み込み手順は決まっているので,テンプレートにする意味はあまりない。とゆことで,インターフェイスのみを抽象化して,読み込む手順は実体を持ったオブジェクトコードとしてまとめるのが良さそうです。

普通に書くとこゆことになるのだと思う。

//////////////////////////////////////////////////////////////////////////////
// これは普通のクラス
class jpeg_reader {
  // 読む処理とか
  // image_reader<CodecT_> と I/F を合わせること。
  // 実装部で IJG jpeglib を使ってもよし,IPP の DCT を使ってもよし。名前は絶
  // 対かち合わない。
};

// 読み込み用のパラメタクラス
class jpeg_reader {
  // 読むパラメタ
  // image_reader<CodecT_> と I/F を合わせること。
};

// 上のふたつをまとめたクラス
class jpeg_codec {
public:
  typedef jpeg_reader reader_type;
  typedef jpeg_writer writer_type;
  typedef jpeg_param param_type;
};

//////////////////////////////////////////////////////////////////////////////
// これはテンプレート
template<typename CodecT_>
class image_reader {
  typedef CodecT_ codec_type;
  typedef typename codec_type::reader_type reader_type;
  typedef typename codec_type::param_type param_type;
public:
  template<typename ImageT_>
  void read(const std::string& fname, ImageT_ image, param_type& param) {
    reader_type reader;
    // ファイルを読む
  }
};

//////////////////////////////////////////////////////////////////////////////
int
main(int argc, char* argv[]) {
  image_reader<jpeg_codec> reader;
  reader.read(...);
  return 0;
}

もちろん,この例がすべてに当てはまる正しい設計というわけではありません。例えば,JPEG を読み込む際に DCT のアルゴリズムを切り替えられるようにしたいと思う場合は,その点も何かしらの方法で抽象化する必要があります。その際,単純にコンパイルスイッチでスイッチできるようなら,その方法も検討できるかもしれません。

とりあえず,自分がプログラムで何をやりたいのか,どこを拡張可能にしておきたいのか,冷静に考えるのが大事なんだと思います。ま,ちょっと思っただけ。

Trackback
Trackback URL:
Ads
About
Search This Site
Ads
Categories
Recent Entries
Log Archive
Syndicate This Site
Info.
クリエイティブ・コモンズ・ライセンス
Movable Type 3.36
Valid XHTML 1.1!
Valid CSS!
ブログタイムズ

© 2003-2012 AIAN