前回こういう記事が発生した。
c++98でもvariantを(バリバリ)使いたい! その1 - in neuro
このときは単に2つの関数をくっつけて即席visitor
を作れるようにして終わったものの、variant
は普通2つ以上詰め込むことが多いので、N個版をゴリゴリ展開していく必要に駆られている。
そもそもなんでこんなことになったのかというと、C++98にはvariadic templateがなく、またラムダもないからだ。
状況説明終わり。
で、variadic templateもないのにどうやって任意個の関数が来た時に対応するかというと、これはもうどうしようもないので、全部書く。2個から上限(例えば20)個まで全部書いて、オーバーロードも全部書く。 でも脳筋でコピペしつづけるのは精神が無理なので、マクロでヴァーっとやる。ほぼこういう状況→動機 - boostjp。
まず、プリプロセッサマクロを使う前に、このページを熟読する(プリプロセッサに関する既知の問題 - boostjp)。実はプリプロセッサマクロは免許制なので、リンク先の記事を読まずに使うとC++警察が現れて逮捕される。嘘です。
そろそろBoost.Preprocessorがヤバイということがわかってきたので、使っていく。そもそもboost::variant
もC++98でvariadic templateをエミュレートするためにマクロを使っており、使える型の上限がboost/variant/variant_fwd.hpp
の中にBOOST_VARIANT_LIMIT_TYPES
として定義されている。その値はBOOST_MPL_LIMIT_LIST_SIZE
だ。Boost.MPLはC++98縛りでC++を書くときには使わずにいられないので当然知っているものとし説明は省略する。知らない人はメタプログラミング関係のアレということで納得しておいてください。
とりあえず、前回作ったシンプルなアレを見てみる(ちゃんとアサートをしないと実用としては厳しいエラーが出たりバグが出るので皆はちゃんとやろう)。
template<typename T0, typename T1> struct aggregate_functions2 : T0, T1 { typedef typename T0::result_type result_type; aggregate_functions2(T0 t0, T1 t1): T0(t0), T1(t1){} using T0::operator(); using T1::operator(); };
複数個に対応した時に変わるべき部分に雑に点々をいれていく。
template<typename T0, typename T1, ..., typename Tn> // <- 1. struct aggregate_functionsN : T0, T1, ..., Tn // <- 2. { // 全result_typeが等しいかチェックするべきなんだけど記事にすると長い typedef typename T0::result_type result_type; aggregate_functionsN(T0 t0, T1 t1, ..., Tn tn) // <- 3. : T0(t0), T1(t1), ..., Tn(tn) {} // <- 4. using T0::operator(); // <- 5. using T1::operator(); ..., using Tn::operator(); };
5箇所、任意個に展開すべき部分がある。順に考えていこう。ここからはBOOST_PP_ENUM
とその変種をゴリゴリ使っていく。これはマクロにカウンタと引数を渡して所定の回数コンマ区切りで展開してくれるやつで、つまり
#define HOGE(z, n, type) type void func(BOOST_PP_ENUM(100, HOGE, int));
のようにするとvoid func(int, int, ...)
という100個のint
を引数に取る関数が簡単に宣言できる。そんな関数を作るな。ちなみにマクロ内で当然ながらz
とかn
も使える。これは、z
が総リピート回数、n
が現在のカウンタになっている。
まず、かなり簡単に片が付くところを潰してしまおう。template
引数は単にtypename Tm
というのを作ればいいので余裕。同様の理由で継承する奴らも余裕。つまり1., 2.は余裕。
実はこの辺りの余裕な奴らというのはマクロを新しく宣言する必要すらない。BOOST_PP_ENUM_PARAMS
というのは、BOOST_PP_ENUM_PARAMS(3, arg)
とするとarg0, arg1, arg2
になる。ので、
#define MAKE_AGGREGATE_FUNCTIONS(Z, N, DUMMY)\ template<BOOST_PP_ENUM_PARAMS(N, typename T)>\ // <- 1. struct aggregate_functionsN : BOOST_PP_ENUM_PARAMS(N, T)\ // <- 2. {\ typedef typename T0::result_type result_type;\ \ aggregate_functionsN(T0 t0, T1 t1, ..., Tn tn)\ // <- 3. : T0(t0), T1(t1), ..., Tn(tn) {} \ // <- 4. using T0::operator(); \ // <- 5. using T1::operator();\ ..., using Tn::operator();\ };\ /**/
ここで、これ全体を後でBOOST_PP_ENUM
で展開する気なので、全体をマクロ定義にした。考えるのだるかったのでprefixをつけてないけどこういうことをやってはいけない。
あとなんか使わない変数をどうするか少し迷って単に放置してるけど多分やり方はちゃんとあると思う。
残ったやつのうち、3.はマクロ定義なしでできる。というのもBOOST_PP_ENUM_BINARY_PARAMS
がBOOST_PP_ENUM_BINARY_PARAMS(3, type, arg)
のようにするとtype0 arg0, type1 arg1, type2 arg2
と展開してくれるからだ。4.と5.は多少面倒な気がする。これらはマクロを使う。どうせ即undef
するので適当に書く。ところでユーザーが使わないであろうマクロを最後にundef
していないライブラリの作者はC++警察の手によって終身刑となり、その後の人生を不用意なマクロによって起きた謎のコンパイルエラーを修正する刑罰に費やすことになる。というのは嘘で、最近のコンパイラはマクロ展開時のエラーを割と教えてくれる。刑罰も嘘です。
#define MAKE_BASECLASS_INIT(z, n, name)\ BOOST_PP_CAT(name, n)(BOOST_PP_CAT(t, n))\ /**/ #define MAKE_USING_OPERATOR(z, n, name)\ using BOOST_PP_CAT(name, n)::operator();\ /**/
で、これをBOOST_PP_ENUM
にぶち込むと謎のマクロ定義が完成する。
#define MAKE_AGGREGATE_FUNCTIONS(Z, N, DUMMY)\ template<BOOST_PP_ENUM_PARAMS(N, typename T)>\ // <- 1. struct aggregate_functionsN : BOOST_PP_ENUM_PARAMS(N, T)\ // <- 2. {\ typedef typename T0::result_type result_type;\ \ aggregate_functionsN(BOOST_PP_ENUM_BINARY_PARAMS(N, T, t))\ // <- 3. : BOOST_PP_ENUM(N, MAKE_BASECLASS_INIT, T) {} \ // <- 4. BOOST_PP_ENUM(N, MAKE_USING_OPERATOR, T) \ // <- 5. };\ /**/
はい。最後にこれ自体を展開するのだけど、0個版が来るとtemplate<>
とかの謎のアレが出てきて奇妙な宣言が発生するので、BOOST_PP_REPEAT_FROM_TO
を使って1個以上から最大個まで展開するようにする。これはBOOST_PP_REPEAT_FROM_TO(1, 10, macro, data)
とするとmacro(10, 1, data) macro(10, 2, data) ... macro(10, 9, data)
になるので、最大個までとなると1先に増やしておく必要がある。
#define AGGREGATE_FUNCTIONS_UPPER_LIMIT BOOST_PP_ADD(1, BOOST_VARIANT_LIMIT_TYPES) BOOST_PP_REPEAT_FROM_TO(1, AGGREGATE_FUNCTIONS_UPPER_LIMIT, MAKE_AGGREGATE_FUNCTIONS, dummy) #undef AGGREGATE_FUNCTIONS_UPPER_LIMIT
あとはmake_visitor
の方も同様にして展開して、不要なマクロを全てundef
しておくのを忘れないように気をつけて終わり。本当はdefine
した直後に下の行にundef
を書いておいて、そのマクロを使う部分はその二行の間に持ってくるのがよい。が、記事だとやりづらいので飛ばしている。
以上のようにして、variantが使いやすくなった。以下のようなコードが書ける。
template<typename T> void doubling(T& arg) {arg *= 2;} void repeat(std::string& str) {str += str;} boost::variant<int, double, std::string> var("hoge"); boost::apply_visitor(make_visitor( resolve(doubling<int>), resolve(doubling<double>), resolve(repeat)), var);
template
関数をそのまま渡すことができないのだけが残念だ。一応、単純なラッパーファンクタをその場で作るマクロなどは思いついてはいるが、マクロなしの解決方法はまだ思いついていない。まあ、この辺りが妥協点だろうか。