C++で構文解析 Boost.Spirit.Qi #3

今回はQiの使いやすさを向上するものを紹介します。

構造体をtupleとして扱えるようにする

パーサに>>演算子を適用すると、属性がtuple<A, B>になることは前回説明しました。でもtupleだと後々ゴチャゴチャしすぎるので、

struct Result {
    A a;
    B b;
};

みたいなのに突っ込めたらとても分かりやすくなってGoodです。Boost.Fusionを使うと出来ます。

// 構造体定義(グローバルでなくてもOK)
struct Result {
    int x;
    double y;
};

// Fusionのマクロ適用(グローバルスコープで行うこと)
BOOST_FUSION_ADAPT_STRUCT(Result, x, y)

BOOST_FUSION_ADAPT_STRUCTマクロには、最初に構造体、それ以降にメンバ名を指定します。これを使うと、Qiが構造体をtupleとみなして扱ってくれます(正確には「構造体をFusionシーケンスに適応させる」が正しい。詳しく知りたい人はググってください)。

std::string str("12345, 67.89");
auto itr = str.begin(), end = str.end();
Result result; // 結果の格納先
bool success = qi::phrase_parse(itr, end, qi::int_ >> qi::lit(",") >> qi::double_, qi::space, result);
if (success && itr == end) {
    std::cout << result.x << std::endl; // 構造体メンバでアクセスできる
    std::cout << result.y << std::endl;
}

こんな風に結果の変数にアクセスできます。メンバには当然名前がついているのでパースした後の処理がとてもやりやすくなります。

余談: 本当は、属性tupleはFusionコンテナであるboost::fusion::vectorのことです。前回std::tuple使用時にインクルードしたboost/fusion/include/std_tuple.hppファイルは、Fusionシーケンスに適応させるためのヘッダファイルとなっています。

rulegrammar

ruleを使用すると、文法に名前をつけることができます。また、grammarを使用するとruleをまとめることができます。

まず、ruleの使用例から。

qi::rule<std::string::iterator, int(), qi::space_type> int_rule;
int_rule = qi::int_;

std::string str("12345, 67.89");
auto itr = str.begin(), end = str.end();
int result;

qi::phrase_parse(itr, end, int_rule, qi::space, result);

qi::ruleのテンプレート引数には、パースする文字列のイテレータの型、属性の型、スキップするパーサの型を指定します。属性の型は、後ろに括弧が必要です。qi::space_typeは、qi::spaceパーサがgrammar(後述)の中に定義されたものだと思ってください。スキップしない(qi::phrase_parse()を使わない)ならこれは省略可能です。

これでint_ruleと名前のついたパーサができました。これは以下のように

int_rule >> qi::lit(",") >> int_rule

既存のものと組み合わせて使用することができます。

これらruleをまとめるgrammarというものがあります。qi::grammarを継承した構造体(クラス)を作成してまとめ上げます。

// 結果の型の定義とマクロ適用
struct Result {
    int x;
    double y;
};
BOOST_FUSION_ADAPT_STRUCT(Result, x, y)

// grammar定義
template <typename Iterator>
struct MyGrammar : public qi::grammar<Iterator, Result(), qi::space_type> {
    MyGrammar() : MyGrammar::base_type(expr) {
        expr = int_parser >> qi::lit(",") >> double_parser;
        int_parser = qi::int_;
        double_parser = qi::double_;
    }

    qi::rule<Iterator, Result(), qi::space_type> expr;
    qi::rule<Iterator, int(), qi::space_type> int_parser;
    qi::rule<Iterator, double(), qi::space_type> double_parser;
};

// パース
std::string str("12345, 3.14");
Result result;
MyGrammar<std::string::iterator> parser;
auto itr = str.begin(), end = str.end();
qi::phrase_parse(itr, end, parser, qi::space, result);
std::cout << result.x << ", " << result.y << std::endl;

基底クラスqi::grammarのテンプレート引数は、qi::ruleと同じです。イテレータの型はよくテンプレート化されます。ローカル変数にqi::ruleを宣言して、コンストラクタ内に文法定義を書きます。基底クラスのコンストラクタの引数は、最初にパースするべき文法を指定します(文法のルートのものとか、最初にパースする非終端記号とか言ったほうがわかりやすい?)。

パースするときに、このgrammarのインスタンスを作成して、qi::parse()またはqi::phrase_parse()に渡すとパースできます。

次回

セマンティックアクションやります。