Boost.Protoぺろぺろ

最初に

このエントリは C++ Advent Calendar jp 2010 : ATND 16日目の記事です.

Boost.Protoぺろぺろ

Boost.ProtoとはBoostに含まれているExpression Template(ET)のためのライブラリです.Xpressiveや,Spirit,Phoenix v3のベースとなっています.このエントリではETについては詳しく触れませんが,式の形を型として保持する手法です.

Boost.Protoについては公式のUsers' Guide - 1.45.0がよくまとまっています.

Boost.Protoは非常に拡張性の高いライブラリです.設計を見習いたいです.ぺろぺろ.

MiniLambda

今回のネタはMiniLambdaです.Boost.Lambdaの簡易版を作ります.まぁ,User's Guideにもそういう話が載っていて,それをベースにしています.

ソースコードhttps://gist.github.com/751465にあります.コンパイルと動作確認はg++ 4.5.1 + Boost 1.45で行いました.

今回作るminilambdaの機能(制限)は,

としています.

includeとか

// minilambda.hpp:4
#include <boost/proto/proto.hpp>
#include <boost/preprocessor.hpp>
#include <boost/fusion/include/vector.hpp>
#include <boost/fusion/include/make_vector.hpp>
#include <boost/fusion/include/at.hpp>
#include <boost/mpl/int.hpp>
#include <boost/utility/enable_if.hpp>

namespace minilambda {

namespace proto = boost::proto;
namespace fusion = boost::fusion;
namespace mpl = boost::mpl;

using proto::lit;

今回作るminilambdaの名前空間はminilambdaとします.proto::litは,ライブラリを使う側のコードで使う可能性があるので,minilambda名前空間にusingしておく方が使いやすいと思います.boost::spirit::litもproto::litのusingです.

expression

// minilambda.hpp:23
template <class Expr>
struct expression;

struct grammar
    : proto::or_<
          proto::plus<grammar, grammar>,
          proto::terminal<proto::_>
      > {
};

struct domain
    : proto::domain<proto::generator<expression>, grammar> {
};

// minilambda.hpp:40
template <class Expr>
struct expression
    : proto::extends<Expr, expression<Expr>, domain> {
    typedef proto::extends<Expr, expression<Expr>, domain> base;

    expression(Expr const& expr)
    : base(expr) {
    }

    template <class... Args>
    int operator ()(Args&&... args) const {
        return proto::eval(*this, context<Args...>(std::forward<Args>(args)...));
    }
};

まず,domainを定義します.domainによって式と文法が関連付けられます.そしてminilambda用の式の型としてexpressionを用意します.Protoの式(この場合Expr)をラップする形になり,expressionに対して独自の動作を加えていきます.独自の動作として,operator ()によってevalを実行できるようにしています.(eval, contextについては次章) grammarはその名の通り文法を表します.grammarに定義されていない式はコンパイルエラーとなります.(_1 - _2とか)

context

// minilambda.hpp:55
template <class... Args>
struct context {
    explicit
    context(Args&&... args)
    : args_(fusion::make_vector(std::forward<Args>(args)...)) {
    }

    /* 中略 */

private:
    decltype(fusion::make_vector(std::declval<Args>()...)) args_;
};

contextは評価器の状態+評価器そのものを表します.minilambdaの場合,プレースホルダーを実装するために,ラムダ式が呼び出された時の引数を状態として持つ必要があります.せっかくのC++0xなのでVariadic Templatesで何個でも持てるようにしましょう(実際はfusion::vectorの要素数に上限がありますが).fusion::vectorとは書けないのでfusion::make_vectorの返り値型を使って変数を定義します.

予め定義されたcontextとして,proto::default_contextやproto::callable_contextがあります.詳しくはUser's Guideを(略).実は今回のminilambdaの用途ではcallable_contextを使った方が楽です.が,それだとUser's Guideと同じものになってしまうので….

// minilambda.hpp:62
    template <
        class Expr,
        class Enable = void
    >
    struct eval;

    template <class Expr>
    struct eval<
        Expr,
        typename boost::enable_if<
            proto::matches<
                Expr,
                proto::terminal<proto::_>
            >
        >::type
    > {
        typedef int result_type;

        result_type
        operator ()(Expr& expr, context const& ctx) const {
            return as_value(proto::value(expr), ctx);
        }

    private:
        int as_value(int value, context const&) const {
            return value;
        }

        template <class I>
        auto as_value(placeholder<I> const&, context const& ctx) const -> decltype(fusion::at<I>(ctx.args_)) {
            return fusion::at<I>(ctx.args_);
        }
    };

    template <class Expr>
    struct eval<
        Expr,
        typename boost::enable_if<
            proto::matches<
                Expr,
                proto::plus<proto::_, proto::_>
            >
        >::type
    > {
        typedef int result_type;

        result_type
        operator ()(Expr& expr, context const& ctx) const {
            return proto::eval(proto::left(expr), ctx) +
                proto::eval(proto::right(expr), ctx);
        }
    };

一番ややこしい部分です.proto::evalを呼ぶと引数のcontextから,context::evalの型が評価関数として使われます.Exprの型によって処理を分岐していきたいのでSFINAE先生の出番となります.

proto::matchesは式に対するパターンマッチを書く事ができます.例えば,proto::_は任意の式にマッチし,proto::terminalは任意の終端記号にマッチします.このあたりのシステムは,元々はパターンマッチというより,式の変形(Transform)に使うもののようですが,僕はよく理解していません.

proto::valueはterminalの値を取る関数です.fusion::result_ofのように,proto::result_of::valueという返り値型が取れるメタ関数がありますが,C++0xではdecltypeが使えるので,あまり必要ではないかもしれません.proto::left, proto::rightは0番目の子,1番目の子ノードを取る関数です.こちらもresult_of::left, result_of::rightが定義されています.

placeholder

宣言は20行目にあります.placeholder >のように,MPL定数を抱えて使うようにします.

// minilambda.hpp:20
template <class I>
struct placeholder : I {};

そして定数は119行目から定義しています.

// minilambda.hpp:119
#define MINILAMBDA_DEFINE_PH(z, n, data)                                \
    auto const BOOST_PP_CAT(_, BOOST_PP_INC(n)) = proto::make_expr<proto::tag::terminal, domain>( \
        placeholder<mpl::int_<n> >()                                    \
    );
BOOST_PP_REPEAT(5, MINILAMBDA_DEFINE_PH, data)

_1, _2, _3, ... を終端記号(terminal)として定義します.protoの式を作るにはmake_expr関数を使います.terminal以外にもplusなど全ての式を作ることができます.make_exprにdomainを指定するとdomainが持つgeneratorの式でextendしてくれます.今回の場合,minilambda::expressionで囲われた型で式が作られます.

最後にmain

#include <iostream>
#include <boost/assert.hpp>
#include "minilambda.hpp"

int main() {
    namespace m = minilambda;

    BOOST_ASSERT(
        (m::_1 + m::_1)(100) == 200
    );

    BOOST_ASSERT(
        (m::_1 + 1)(100) == 101
    );

    BOOST_ASSERT(
        (m::_1 + m::_2)(100, 200) == 300
    );

    return 0;
}

テストコードを兼ねたmainです.

最後に

僕は説明とか,文書を書くとか,まとめを書くとかが下手なので,わかりにくい部分が多々あると思います.つっこみなどはTwitterかコメント欄でお願いします.

文章がボロボロすぎですね!