C++98でもvariantを(バリバリ)使いたい! その2

前回こういう記事が発生した。

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::variantC++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_PARAMSBOOST_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関数をそのまま渡すことができないのだけが残念だ。一応、単純なラッパーファンクタをその場で作るマクロなどは思いついてはいるが、マクロなしの解決方法はまだ思いついていない。まあ、この辺りが妥協点だろうか。