読者です 読者をやめる 読者になる 読者になる

Boost.MPL で条件の分岐先の評価を遅延したい

C++ Boost

Boost.MPL でテンプレートの評価を遅延するやり方が分かっていなかったので調べてみたメモ. 具体例で書く.

ある正数からその正数以下の連番文字列を作り出すメタ関数を考える.例えば,5 を入力すると "12345" が得られるものとする.面倒くさいので,入力は1桁の正数に限定する.

何も考えずに書くとこうなった:

#include <iostream>

#include <boost/mpl/if.hpp>
#include <boost/mpl/size_t.hpp>
#include <boost/mpl/char.hpp>
#include <boost/mpl/string.hpp>
#include <boost/mpl/identity.hpp>

using namespace boost;

template<size_t N>
struct renban_string
    : mpl::if_c< N==0, mpl::string<>,
        mpl::push_back<
            typename renban_string<N-1>::type,
            mpl::char_<'0' + N>
        >
    >::type
{
    static_assert(N < 10, "");
};

int main()
{
    std::cout << mpl::c_str<typename renban_string<7>::type>::value << std::endl;
    return 0;
}

でもこれは「テンプレートのインスタンス再帰回数が限界」と言われてうまくいかない.理由は簡単で,mpl::if_c<> で,条件式 N==0 の真偽に関わらず renban_string<N-1>mpl::char_<'0' + N> のどちらも評価されてしまうからだ.

普通の C++ で書くとこういうイメージになる(戻り値型がまずいのはスルーしてください)

template<class T, class U>
auto if_(bool cond, T t, U u) // この関数を呼び出した時点で t も u も評価される
{
    return cond ? t : u;
}

そこで,前なんか mpl::eval_if というのを教えてもらった気がしてきて,それを試してみる.mpl::eval_ifmpl::if_ とは違い,値ではなく,値を返すメタ関数を取る. イメージ的にはこう.

template<class F1, class F2>
auto enable_if(bool cond, F1 f1, F2 f2) // f1 と f2 は値を返す関数なので,引数の時点では関数の中身は評価されない
{
    return (cond ? f1 : f2)(); // 条件式で f1 と f2 のどちらかを引数なしで呼び出すことで値を取り出す
}

しかし,push_back<> を評価するには引数の renban_string<> を評価しないといけないのでそのままでは評価を遅延させる事ができず,

template<size_t N>
struct renban_string {
    static_assert(N < 10, "");

    template<size_t N2>
    struct push_back_char
        : mpl::push_back<
            typename renban_string<N2-1>::type,
            mpl::char_<'0' + N2>
        >::type
    {};

    typedef typename mpl::eval_if_c<
        N==0,
        mpl::string<>,
        push_back_char<N>
    >::type type;

};

のように,typename renban_string<>::type と評価している部分を外に追い出してやると mpl::eval_if_c の else 節の renban_string_impl<> は必要な場合以外評価されなくなりとりあえずはうまく動いた.メタ関数を呼ぶには引数を評価しないといけないので,引数の評価を別メタ関数でやれば良いのは当然な気もするけれど,別のメタ関数を作らないといけないのはいまいちなのでもうちょっと考えてみる.

push_back<> を適用した結果を渡せないのなら,push_back<> の引数に渡す値を分岐すると良い.

template<size_t N>
struct renban_string
    : mpl::push_back<
        typename mpl::eval_if_c<
            N==1,
            mpl::string<>,
            renban_string<N-1>
        >::type,
        mpl::char_<'0' + N>
    >::type
{
    static_assert(N < 10, "");
};

けどこれも N==1 と帳尻を合わせたりしていて,常にこの手が使えるわけでもないし,ちょっといまいちな気がする(前よりはマシだけれど)

('、3_ヽ)_

なんか色々間違ってるかも. こんな初歩的なこと多分多くの人が通っていると思うので,もっとスマートなやり方がある気もするけれど,探してもなかなか見つからず力尽きた.