MPL もそうとうでかいので一回で紹介しきるのは厳しそうなので、 何回かに分けて紹介できればなあ、と思います。
template library を作る で紹介した、 GameNum ですが、 これは、テンプレートメタプログラミングの初歩の内容を含んでいたので、 MPL で書き直したらどうなるか、というのをやってみます。 まあ、最初にやってみるネタとしては適当かなあ、とか思いまして。
gamenum のシフトをコンパイルタイムに解決する部分、
template <bool equal_, bool leftLarge_, int leftShifts_, int rightShifts_> struct ConvertHelper_; template <int leftShifts_, int rightShifts_> struct ConvertHelper_<true, false, leftShifts_, rightShifts_> { static Int_ run(Int_ v) { return v; } }; template <int leftShifts_, int rightShifts_> struct ConvertHelper_<false, true, leftShifts_, rightShifts_> { static Int_ run(Int_ v) { return v << leftShifts_ - rightShifts_; } }; template <int leftShifts_, int rightShifts_> struct ConvertHelper_<false, false, leftShifts_, rightShifts_> { static Int_ run(Int_ v) { return v >> rightShifts_ - leftShifts_; } }; template <int leftShifts_, int rightShifts_> Int_ convert_(Int_ v) const { return ConvertHelper_ <(leftShifts_ == rightShifts_), (leftShifts_ > rightShifts_), leftShifts_, rightShifts_>::run(v); }
を MPL を使って書き直してみるのが、今回の主旨です。
まず、実際のシフトする部分をポリシーとして書いてみます。
struct NoShiftPolicy { static Int_ shift(Int_ v) { return v; } }; template <int leftShifts_> struct LeftShiftPolicy { static Int_ shift(Int_ v) { return v << leftShifts_; } }; template <int rightShifts_> struct RightShiftPolicy { static Int_ shift(Int_ v) { return v >> rightShifts_; } };
うん、ここまでは非常に簡単。
さて、問題は Int_ convert_(Int_) const ですが 実行時だと思ってどういう処理をすべきかを考えると、
template <int leftShifts_, int rightShifts_> Int_ convert_(Int_ v) const { if (rightShifts_ == leftShifts_) { return NoShiftPolicy::shift(v); } else { if (leftShifts_ > rightShifts_) { return LeftShiftPolicy<leftShifts_-rightShifts_>::shift(v); } else { return RightShiftPolicy<rightShifts_-leftShifts_>::shift(v); } } }
と、いうようなことをすれば良いことが解ります。
このへんでメタプログラミングに詳しくない人は、 MPL Mini-tutorial を読むことを勧めます。 私が雑に 日本語で概略 を書いているので、それも参照して頂ければ幸いです。
とにかく重要なのは、コンパイルタイムでは、
値の代入 == typedef 関数の引数 == template 引数 関数の発動 == template クラスのインスタンシエイト 関数の戻り値 == template クラス内での typedef
なんだ、という図式を頭に叩き込むことです。
さて、先程の実行時の計算をコンパイルタイムで行うのですが、 MPL では if 文をコンパイルタイムで行うためのものとして、 if_, if_c, apply_if, apply_if_c を用意しています。 apply_if_c が普通の if だと思えばいいと思います。 んで、_c が付いていないものは型で bool 値を表す、 type_traits を判定に使うときに使うもので、 apply_ が付いているのは、 if の中身を実際に条件式の真偽が確定された時まで 評価を遅らせてくれるものです。
今回は別に遅延する必要も無いので、if_c を用います。
if_c は三テンプレート引数を取り、 第一テンプレート引数は bool 値で、 その値が真なら第二テンプレート引数に指示された型を if_c::type として返し、 偽なら第三テンプレート引数の型を if_c::type として返します。
例えば、
boost::mpl::if_c <(sizeof(int) == 8), int, long long int>::type;
という型は 64ビットマシンでは int、 それ以外のマシンでは long long int を表すことになります。
さあ、ここまで来れば convert_ の定義も理解できるでしょう。
template <int leftShifts_, int rightShifts_> Int_ convert_(Int_ v) const { return boost::mpl::if_c < (rightShifts_ == leftShifts_), NoShiftPolicy, boost::mpl::if_c < (leftShifts_ > rightShifts_), LeftShiftPolicy<leftShifts_-rightShifts_>, RightShiftPolicy<rightShifts_-leftShifts_> >::type >::type::shift(v); }
まあ、という訳です。ちなみに、 GameNum with MPL
MPL第二回です。今度はシーケンスを使ってみます。 MPLのコンセプトはSTLに非常に酷似していますので、 c++使いにはコンセプト段階では理解しやすいと思います。 逆にSTLをあまり理解していない方には難しいかもしれません。
Javaの何が妬ましいかって、いろいろありますが、Serialize が羨しい。 正確にはリフレクションが羨しいってことなんだけど。 なかなかリフレクション無しには作れない機構です。
で、Serializer を作ろうかとも思ったのですが、 これは非常に大変なので、boost にお任せすることにして、 とりあえず、オブジェクトの中身を 汎用的に出力するものでも作ろうとかと思います。 まあ、デバッガかなんかを使えばいいんですが、 エラー時なんかにオブジェクトの中身を 吐くようにしておけばデバッグもしやすいし、 バグレポートも送ってもらいやすいし、色々便利な気がします。
で、インターフェイスですが、 まあ、以下の二つともできるようにしましょう。
class Dumpable : public Dumper<...> {}; class NotDumpable {}; int main() { Dumpable d; d.dump(); NotDumpable nd; Dumper<...>::dump(&nd); }
さて、問題になるのが、 ... の部分。 こいつは、直感的には、Dumper<Dumpable> としたいわけです。 ただ、c++ にはリフレクションが無いので、 Dumpable からメンバを調べる手段が無く、 手詰まりになってしまいます。 だから、この場合は、クライアントコード側で、 メンバの配置を教える必要があります。
つまり、Dumper<char, int, std::string> などとすれば良いのです。 しかし、ここでまた問題が出てきます。 template 引数は、可変個数引数をサポートしていないのです。 一引数バージョン、二引数バージョン…と多重定義をいくつも重ねれば 実現可能ですが(実際 Boost.bind はそんな感じです)、非常に面倒くさい。 長くなりましたが、こんな時こそ MPL Sequence/Iterator の出番です。 MPL Sequence を使うと、クライアントコード側では、 Dumper<boost::mpl::list<char, int, std::string> > といった具合になります。
さて、仕様が決まったところで、実際に MPL を使った実装を見てみましょう。 まず、Dumper のインターフェイスはこんな感じです。
// めんどくさいので以下これを仮定します。 namespace mpl = boost::mpl; template <class Seq_> class Dumper { public: static void dump(void* p, std::ostream& os =std::cout); void dump(std::ostream& os =std::cout) { void* ptr = (void*)this; dump(ptr, os); } };
メンバの方の dump は static な dump に丸投げで良いので既に実装しました。 で static な dump の方ですが、 実装すべきコードを擬似コードで考えてみると、
for (Seq_::iterator ite = Seq_::begin; ite != Seq_::end; ite++) { // ite からタイプを調べて、タイプに応じてダンプ処理。 // ite からタイプを調べて、タイプに応じてポインタを進める。 }
といったところでしょう。 コンパイルタイムなループ処理は、 MPL Algorithm として用意されていますが、 ここでは、Sequence/Iterator の練習ということで (まあ単にまだ私があまり Algorithm を理解してないだけとも言いますが)、 ループ処理は自前で書くことにします。
さて、メタプログラミングでは、 当然 for ループなどが用意されていないので、 ループ処理をメタ関数の再帰呼び出しによって行うことになります。
さて、ここで以下の公式を思い出します。
関数の引数 == template 引数 関数の発動 == template クラスのインスタンシエイト
ということで、iterator を引数にとる template クラスを作れば良さそうです。
template <class Begin_, class End_> struct DumpImpl { typedef typename Begin_::type Type; static void dump(void* p, std::ostream& os =std::cout) { // Type を使ってダンプ処理。 DumpImpl<typename Begin_::next, End_>::dump(p, os); } };
MPL イテレータは Iterator::next で次のイテレータが取得できます。 また Iterator::type で「デリファレンス」が行えます。 これで、再帰的に DumpImpl がインスタンシエイトされることがわかります。 ダンプ処理は、汚ならしいし、MPLと関係無いし、 何よりもプラットホームに依存しまくるのでここでは述べません。 内容が見たければ、最後で紹介する実際のコードを見て下さい。
で、最後の仕上げです。 このままでは再帰処理が終了しませんので、 Begin_ == End_ の時は何もしない、という処理が必要です。 これには、テンプレート特別バージョンを用いて以下のようにします。
template <class End_> struct DumpImpl<End_, End_> { static void dump(void*, std::ostream&) {} };
これで OK です。
再帰関数ができたところで、元の関数 dump の実装です。 MPL Sequence は mpl::begin<Sequence>::type と、 mpl::end<Sequence>::type で それぞれ、先頭と最後尾の後ろの番犬のイテレータが取得できます。 だから…
void dump(std::ostream& os =std::cout) { void* ptr = (void*)this; DumpImpl<typename mpl::begin<Seq_>::type, typename mpl::end<Seq_>::type>::dump(p, os); }
で実装完了です。
で、今までのコード。
dumper.h (for gcc or icc only)。
テストコード
テストコードを実行すると、比較用のダンプと共に、
3221222032: derived from string 3221222036: a 3221222040: 2 3221222044: hello world 3221222048: b
なんていう出力が得られましたとさ。 なんかもうちょっと工夫した出力をしろ、って感じですな。
ちなみに、for gcc or icc と書いたのは、 オブジェクトのメモリレイアウトがコンパイラごとに異なるからです。 gcc-2系列と、gcc-3系列でも異なりました。 gcc-3系列は非常に常識的なレイアウトだったので、 他のコンパイラでも通るかもしれません。 ちなみに icc と gcc-3 は同じでした。 kylix3 は驚く程違ったので、とりあえず放棄しました。 動作確認したコンパイラは、 gcc-2.95.3, gcc-2.96, gcc-3.0.4, gcc-3.1, gcc-3.2, intel-icc6 です。
ああ、あと gcc-2 系列では継承数でメモリレイアウトが変化するので、 テンプレート第二引数で継承の数を渡しています。
TODO
全てリンクフリーです。 コード片は自由に使用していただいて構いません。 その他のものはGPL扱いであればあらゆる使用に関して文句は言いません。 なにかあれば下記メールアドレスへ。