本記事は、Boost Advent Calendar 2011 (リンク切れ http://partake.in/events/597a0fc3-0e3a-47a3-8fc3-4f32ad846a3d)の17日目の記事です。


きっかけは、「bind(BoostやTR1やC++11にあるやつ)を書いてみよう」と思い立ったことでした。

常日頃から”shared_ptr”, “function”, “bind”が三種の神器であると考えている私ですが、今までbindだけは自分で作ったことがなく、また作り方も分からなかったのです。そのためBoost/TR1/C++11なしの縛りでコードを書く際は、std::bind1st/bind2ndが適用できるよう落とし込むか、その都度関数オブジェクトを作る羽目になっていました。

しかし、最近ようやく1つのやり方を思い付きました(mem_fn相当を自動でやってくれる機能は本筋ではないので省略しています)。ちなみに、未だにBoost.Bindのソースコードは見たことがありません。

  1. Bind関数では、引数リストをタプルに保管したbinderオブジェクトを返す(bind(f, foo, _1, bar, _2)なら{foo, _1, bar}というタプルにする)。
  2. operator ()では次のことを行う。

    1. binderのタプルから_1(プレースホルダ)に1番目の引数を当てはめたタプルを作る。
    2. 上のタプルから_2に2番目の引数を当てはめたタプルを作る。
    3. 「上の_NにN番目の引数を当てはめたタプルを作る」を引数が尽きるまで繰り返す
    4. 上のタプルを引数並びとして関数を呼び出す。

「タプルを操作する」といえばBoost.Fusionです。とりあえずFusionを使って、上記のようなプログラムが書けることを試すことにしました。ここまで、前書きでした。今回の話にはbindを作ること自体は特に関係ありません。


Boost.Fusionを使ってライブラリを作るということは、戻り値の型を書くこととの戦いと言っても過言ではありません。これが今日の本題です。

そのbindを書いていて次のようなコードが出てきました。

template<
  std::size_t N,
  typename ArgsSequence,
  typename Args2Sequence,
  typename boost::enable_if<
    typename mpl::empty<Args2Sequence>::type>::type*& = enabler>
(……)
invokeN(ArgsSequence const& s, Args2Sequence const&) const
{
  return fusion::invoke(m_f, s);
}

fusion::invokeは「タプル(Fusion Sequence)を引数並びとして関数を呼び出す」を実現してくれるものです。さて、invokeNメンバ関数の戻り値の型、(……)には何を当てはめればよいでしょうか。すなわち、このfusion::invokeはどんな型を返すのでしょうか。もちろん、C++11ならdecltypeが使えます(注意:以下のコードは実際にはコンパイルエラーになります)。

template<
  std::size_t N,
  typename ArgsSequence,
  typename Args2Sequence,
  typename boost::enable_if<
    typename mpl::empty<Args2Sequence>::type>::type*& = enabler>
auto invokeN(ArgsSequence const& s, Args2Sequence const&) const
  -> decltype(fusion::invoke(m_f, s))
{
  return fusion::invoke(m_f, s);
}

しかし、decltypeのないC++03ではどうしましょう?この問題に立ち向かっているのが、Boost.Fusionのもう1つの顔なのです。

基本的には、戻り値の型を別手段で提供しています。もう1度fusion::invokeの公式のページを見てみましょう:infoke

Synopsis

template<
    typename Function,
    class Sequence
    >
typename result_of::invoke<Function, Sequence>::type
invoke(Function f, Sequence & s);
 
template<
    typename Function,
    class Sequence
    >
typename result_of::invoke<Function, Sequence const>::type
invoke(Function f, Sequence const & s);

戻り値の型はtypename result_of::invoke<……>::typeとして宣言されています。そう、これがミソなのです。

Boost.Fusionにあるすべての関数は、boost::fusion::result_of::(関数名)<関数の引数の型>::typeというメタ関数呼出で戻り値の型が取得できるようになっているのです。

そうと分かれば簡単です。M_fの型はFunctorであるとして、次のように書けば良いのです。

template<
  std::size_t N,
  typename ArgsSequence,
  typename Args2Sequence,
  typename boost::enable_if<
    typename mpl::empty<Args2Sequence>::type>::type*& = enabler>
typename fusion::result_of::invoke<Functor, ArgsSequence>::type
invokeN(ArgsSequence const& s, Args2Sequence const&) const
{
  return fusion::invoke(m_f, s);
}

先ほどの(……)に果てはまる答えは、typename fusion::result_of::invoke<Functor, ArgsSequence>::typeでした。

ただし、これで完成にしてはいけません。このinvokeN関数の戻り値の型の取得手段も、やはり提供しましょう。ただ、fusionほど厳密にやらないことにします(invokeNは外部に公開する関数という位置づけではないからです)。

template<typename ArgsSequence>
struct invoke0_result :
  fusion::result_of::invoke<Functor, ArgsSequence>
{};

ArgsSequenceさえ分かれば決定できるということで、それだけしか引数にしていません。名前もresult::invokeNとしていません。

これを使って先ほどの例は次のように直します。

template<
  std::size_t N,
  typename ArgsSequence,
  typename Args2Sequence,
  typename boost::enable_if<
    typename mpl::empty<Args2Sequence>::type>::type*& = enabler>
typename invoke0_result<ArgsSequence>::type
invokeN(ArgsSequence const& s, Args2Sequence const&) const
{
  return fusion::invoke(m_f, s);
}

では、次の例にいってみましょう。

template<
  std::size_t N,
  typename ArgsSequence,
  typename Args2Sequence,
  typename boost::disable_if<
    typename mpl::empty<Args2Sequence>::type>::type*& = enabler>
(戻り値の型2)
invokeN(ArgsSequence const& s, Args2Sequence const& args2) const
{
  typedef typename boost::remove_reference<typename fusion::result_of::front<Args2Sequence>::type>::type front_type;
  return this->invokeN<N + 1>(
    fusion::transform(s, replace<placeholder<N>, front_type>{fusion::front(args2)}),
    fusion::pop_front(args2));
}

(戻り値の型2)はどう書いたら良いでしょうか。関数の中身はさっきより複雑です。

今度は、初めから戻り値型を得るメタ関数から作ることにします。

template<
  std::size_t N,
  typename ArgsSequence,
  typename Args2Sequence>
struct invokeN_result :
  (何か)
{};

さてお気づきかもしれませんが、最初の例の関数と今度の例の関数、同じ名前です。enable_ifとdisable_ifで切り替えているのです。Args2Sequenceが長さ0なら最初の関数、そうでなければ、こっちの関数を使うよう分岐させていました。なので、それを表現します。

template<
  std::size_t N,
  typename ArgsSequence,
  typename Args2Sequence>
struct invokeN_result :
  mpl::eval_if<
    typename mpl::empty<Args2Sequence>::type,
    invoke0_result<ArgsSequence>,
    (こっちのinvokeNが返す型)
  >
{};

Mpl::eval_ifの条件はenable_if/disable_ifの条件をそのまま持ってきました。そして真の場合は先ほどのinvoke0_resultがさっそく登場です。そう、このinvokeN_resultは、今度のinvokeNだけでなく、最初のinvokeNにも対応した「invokeNの戻り値を表すメタ関数」に変貌したのです。

invokeNの中身を見てみましょう。

return this->invokeN<N + 1>(

そう、再帰呼出です。なので、invokeNの戻り値の型を知るには「invokeNの戻り値を表すメタ関数」が必要という状況に陥っていたのです。

template<
  std::size_t N,
  typename ArgsSequence,
  typename Args2Sequence>
struct invokeN_result :
  mpl::eval_if<
    typename mpl::empty<Args2Sequence>::type,
    invoke0_result<ArgsSequence>,
    invokeN_result<
      N + 1,
      (……), 
      (……),
    >
  >
{};

というわけで、さっそくそこを埋めました。invokeN_resultは3つ引数を取るメタ関数なので、残り2つが問題です。再びinvokeN内を見ていきましょう。

template<
  std::size_t N,
  typename ArgsSequence,
  typename Args2Sequence>
return this->invokeN<N + 1>(
  fusion::transform(s, replace<placeholder<N>, front_type>{fusion::front(args2)}),
  fusion::pop_front(args2));

では、1つ目の引数、Fusion::transformです。Sの型はArgsSequenceです。さっそく書き込みます。

template<
  std::size_t N,
  typename ArgsSequence,
  typename Args2Sequence>
struct invokeN_result :
  mpl::eval_if<
    typename mpl::empty<Args2Sequence>::type,
    invoke0_result<ArgsSequence>,
    invokeN_result<
      N + 1,
      typename fusion::result_of::transform<
        ArgsSequence,
        (……)
      >::type,
      (……)
    >
  >
{};

ここのFusion::transformの2つ目の引数replace<placeholder<N>, front_type>{fusion::front(args2)}は、replace<placeholder<N>, front_type>型の一時オブジェクトを作りfusion::front(args2)で初期化という式です(C++11の初期化構文を使用)。なので、fusion::front(args2)は知らなくても大丈夫です(とは言え、実際にはfront_typeなのですけどね)。

template<
  std::size_t N,
  typename ArgsSequence,
  typename Args2Sequence>
struct invokeN_result :
  mpl::eval_if<
    typename mpl::empty<Args2Sequence>::type,
    invoke0_result<ArgsSequence>,
    invokeN_result<
      N + 1,
      typename fusion::result_of::transform<
        ArgsSequence,
        replace<placeholder<N>, front_type>
      >::type,
      (……)
    >
  >
{};

さて、これで良いと思ったらそれは早とちりです。front_typeはinvokeN内のtypedefだったのでここでは参照できません。front_typeの定義を見て直接書き込むことにします。

template<
  std::size_t N,
  typename ArgsSequence,
  typename Args2Sequence>
struct invokeN_result :
  mpl::eval_if<
    typename mpl::empty<Args2Sequence>::type,
    invoke0_result<ArgsSequence>,
    invokeN_result<
      N + 1,
      typename fusion::result_of::transform<
        ArgsSequence,
        replace<
          placeholder<N>,
          typename boost::remove_reference<
            typename fusion::result_of::front<Args2Sequence>::type
          >::type
        >
      >::type,
      (……)
    >
  >
{};

残った(……)に対応するのは、fusion::pop_front(args2)です。もう分かりましたね。fusion::result_of::pop_frontを使います。これでinvokeN_resultが完成しました。

template<
  std::size_t N,
  typename ArgsSequence,
  typename Args2Sequence>
struct invokeN_result :
  mpl::eval_if<
    typename mpl::empty<Args2Sequence>::type,
    invoke0_result<ArgsSequence>,
    invokeN_result<
      N + 1,
      typename fusion::result_of::transform<
        ArgsSequence,
        replace<
          placeholder<N>,
          typename boost::remove_reference<
            typename fusion::result_of::front<Args2Sequence>::type
          >::type
        >
      >::type,
      typename fusion::result_of::pop_front<Args2Sequence>::type
    >
  >
{};

これを使って今度のinvokeNは次のような戻り値の型を書けば良いのです。

template<
  std::size_t N,
  typename ArgsSequence,
  typename Args2Sequence,
  typename boost::disable_if<
    typename mpl::empty<Args2Sequence>::type>::type*& = enabler>
typename invokeN_result<N, ArgsSequence, Args2Sequence>::type
invokeN(ArgsSequence const& s, Args2Sequence const& args2) const

結論です。