yutopp's blog

サンドバッグになりたい

Boost.Spirit.X3のご紹介 - C++ Advent Calendar 2014(16日目)

この記事はC++ Advent Calendar 2014 16日目の記事です.
おはようございます,@yutoppです.

去年のC++ Advent Calendar 2013ではBoost.Spirit.QiのTipsを書きました.今年は,Boost.Spirit.X3の紹介をしたいと思います!

環境は,Antergos Linux(x64) / Clang 3.5.0 / Boost 1.56.0 です.
また,Boost.Spirit.X3はexperimentalとのことなので,この記事の内容はすぐに古くなる可能性があります.

Boost.Spirit.X3とは?

Spirit X3
boostorg/sprit

C++11やC++14の機能を用いて再設計されたBoost.Spirit.Qiの次のexperimentalな実装です.
Classic Spiritのようなシンプルさを取り戻すことを目標にしているとのことです.

spirit/include/boost/spirit/home/x3がお目当てのX3の内容になります.

X3を使ったコードには以下のものがありました.

X3の利点

コンパイルが速い

試しにサンプルをビルドして計測してみました.
Qi Calc1 / Qi Calc2 / Qi Calc4
X3 Calc1 / X3 Calc2 / X3 Calc4

以下の表は,
clang++ -ftime-report -std=c++14 -I /usr/include/boost/ -c 各サンプル名
を10回繰り返し計測したClang front-end timerUser Timeの値(sec)の平均です.

calc1 calc2 calc4
Qi 4.283 4.409 4.96335
X3 1.87333 1.89767 2.34934

大体今までの2倍の速さでコンパイル出来るようになっているようです.体感でわかる程度に速いので良いですね.

エラーがわかりやすい(Qi比)

内部実装がシンプルになったことによって,エラーも分かりやすくなった気がします.

宣言と定義を同時に書けるようになる(?)

X3からruleの内容を先に定義してから,rule自体を定義するようなスタイルになったので,謎マクロを噛ますことで同時に宣言できます(後述します).

C++14を使いまくれる

ライブラリ自体が最低でもC++14を要求するので,このライブラリをincludeした時点でC++14の機能を使わないともったいないという気持ちになれます.

X3の現状の問題点

undocumented

ドキュメントが無いのでつらいです…が,X3の実装がとても読みやすいので,コードを読みながら何とか使えます.
特に変わったのはruleの定義とセマンティックアクションの記述,attrvalの扱いかな,と思います.

定義済みパーサが少ない

仕方がないので自分で書きます.

Tips

X3では,以下のようにruleを定義します.(サンプルより抜粋)

x3::rule<class expression, ast::program> const expression("expression");

auto const expression_def =
            term
            >> *(   ('+' >> term)
                |   ('-' >> term)
                )
            ;

BOOST_SPIRIT_DEFINE(
    expression = expression_def
    );

しかし,個人的にはruleとdefを同時に書きたいわけです. そこで,マクロを噛まします.
実際のコード

#define RULES_BEGIN( struct_name, entrypoint_name )         \
    struct struct_name                                      \
    {                                                       \
    using self_type = struct_name;                          \
                                                            \
    static auto instance()                                  \
        -> struct_name const&                               \
    {                                                       \
        static struct_name self;                            \
        return self;                                        \
    }                                                       \
                                                            \
    static auto entrypoint()                                \
    {                                                       \
        return instance().entrypoint_name;                  \
    }                                                       \

#define RULES_END                          \
    };

#define ANNOTATOR_COL() :

#define ANNOTAROR_BASE_SPEC( r, _unused, index, elem )      \
    BOOST_PP_IIF( BOOST_PP_EQUAL(index, 0),                 \
                  ANNOTATOR_COL,                            \
                  BOOST_PP_COMMA                            \
        )() public elem

#define RULE( name, type, ... )                    \
    RULE_WITH_ANNOTATOR( name, type, BOOST_PP_SEQ_NIL, __VA_ARGS__ )

#define RULE_WITH_ANNOTATOR( name, type, bases, ... )                   \
    class name                                                          \
        BOOST_PP_SEQ_FOR_EACH_I( ANNOTAROR_BASE_SPEC, _, bases )        \
    {};                                                                 \
    struct PP_ ## name {                                                \
        using rule_type = x3::rule<name, type>;                         \
        static auto def( self_type const& t ) {                         \
            return __VA_ARGS__;                                         \
        }                                                               \
        auto operator()() const -> rule_type                            \
        {                                                               \
            return rule_type( #name );                                  \
        }                                                               \
    };                                                                  \
    decltype(( std::declval<PP_ ## name>()) ()) name                    \
        = PP_ ## name()();                                              \
    template <typename Iterator, typename Context, typename Attribute>  \
    friend inline bool parse_rule(                                      \
        decltype(( std::declval<PP_ ## name>()) ()) rule_,              \
        Iterator& first, Iterator const& last,                          \
        Context const& context, Attribute& attr                         \
        )                                                               \
    {                                                                   \
        using boost::spirit::x3::unused;                                \
        auto const& ri = self_type::instance();                         \
        auto const& raw_def = PP_ ## name :: def( ri );                 \
        auto const& def_ = ( ri.name = raw_def );                       \
        return def_.parse(first, last, context, unused, attr);          \
    }

classの中であれば前方参照できるので,ruleは全体的にclassの中で定義するようにします.
ruleの中身はstruct PP_ ## namedefで定義します.引数のtがキモで,ruleを定義する際は,tのメンバ変数として参照するようにします.
また,parse_ruleが呼べればX3から使えるようなので,BOOST_SPIRIT_DEFINEの定義から抜き出してfriendとして定義しておきます.

実際の使い方はこのような感じになります.
実際のコード

RULES_BEGIN( rules, program )

RULE_WITH_ANNOTATOR( program, ast::module_ptr,
    ( on_error_annotator_base ),
    t.module > ( x3::eol | x3::eoi )
    )

RULES_END

RULES_BEGINでは,rulesというクラスを定義して,programから始まるrule群を定義しています.
RULE_WITH_ANNOTATORでは,on_error_annotator_baseに従ってアノテーションを行い,ast::module_ptr型の属性を持つ,programというruleを定義しています.
t.moduleというrule(ここでは定義していませんが…)を参照しています.残りはQiのときと同じですね.

あとはこのように使うだけです.

このマクロを用いると,ruleの定義を1箇所で行えるようになります.X3自体の効果でコンパイル時間も短くなりますし,良さがありますね.

実際に使ってみた

去年の記事にも書いた文鳥言語で,実際にX3を使ってみました.
(文鳥言語はC++14とBoost.Spirit.X3,LLVMを用いて,C++とDlangライクな言語を目指して開発している言語です)

yutopp/rill

Boost.Spirit.X3を使ってみて大きいのは,コンパイル時間が短くなったこと,それとコンパイル時間が短くなったこと,後はコンパイル時間が短くなったことが挙げられます.
エラーメッセージが分かりやすくなったこと,マクロで定義を1箇所に書けるようになったのも大きいかもしれません.
C++14のgeneric lambdaと組み合わせると,セマンティックアクションがとても書きやすいのも魅力でした.

Spirit.Qiでコンパイル時間がBoostするのがつらい皆様,ぜひX3を使ってみませんか.
そしてついでに文鳥言語も開発しませんか?とてもアットホームな鳥小屋です.

おわり

experimentalだけど現段階でも十分魅力的なBoost.Spirit.X3の紹介でした.
後日もう少し書き足します…

イヌヌ
@Linda_ppさんの今年気になった C++ ライブラリとかフレームワークを紹介する記事