今日は,クラス設計に関するお話.
文字列処理を行うプログラムを書いていると,tokenizer(ひとつの文字列を特定の文字を区切り文字として複数の文字列に切り分けること)が必要となる場面に多々出くわします.tokenizerならboostでも使っておけばいいのでしょうが,boostのtokenizerはランダムアクセス(tok[i]みたいな)ができないので個人的にはちょっと使いづらいです.そういう訳で,せっかくなのでtokenizerクラスを作成しようと思い立ちました.
クラスは,こんな感じになります.
template< class String, class TokenFunction, template <class> class Container > class basic_tokenizer { ・・・ };
最初のテンプレート引数で,文字列の型を指定します(std::stringとか).次の引数には,実際に文字列を切り分けるクラスを指定します.この辺は,boostのTokenFunctionがそのまま使えるように実装しようかなと.最後は,切り分けた文字列を格納するためのコンテナ(入れ物)を指定します(std::vectorとか).
今回の話題として取り上げるのは,この3番目のtemplate <class> class Containerという部分です.何故,素直にclass Containerと書かないのでしょうか.
Modern C++ Design,1.5.1 テンプレート・パラメータにtemplateを用いたポリシー・クラスの実装によると(訳本ですが),
// ライブラリ・コード
template <class CreationPolicy>
class WidgetManager : public CreationPolicy
{
・・・
};// アプリケーション・コード
typedef WidgetManager< OpNewCreator<Widget> > MyWidgetMgr;
(中略)
上述の例を見ていただくと分かるように,ポリシーのテンプレート引数は冗長のものとなっています.ユーザがわざわざOpNewCreatorのテンプレート引数(Widget)を引き渡さなければならないのは格好の良いものではありません.テンプレート引数で指定しているポリシー・クラスの情報は,たいていの場合,ホスト・クラスにとっては取得済みのものであるか,簡単に推論できるものなのです.上記の例では,WidgetManagerは常にWidget型のオブジェクトを管理するものとして作られているのです.このため,OpNewCreatorの実体化を行う際にユーザがWidgetを再度指定することは冗長であり,潜在的な危険をもはらんでいることになるのです.
つまり,tokenizerの場合では,Container(入れ物)はStringを格納するようにできていることは分かりきっていることなので,std::vector<std::string>とユーザに書かせることは格好の良いものではない.また,ユーザが間違ってstd::vector<int>などと書いてしまった場合には,予期できない動作を起こす可能性があるためお勧めできない.ということです.
こんな感じでクラス設計とかを考えながら,ガリガリとコードを書くのはやっていてすごく楽しくて好きです(とか書くと,また周りから奇異な目で見られてしまいますがw).