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

右上➚

プログラミングに関するメモをのこしていきます

Boost.Spirit.X3 の練習1

C++ Boost コンパイラ

Boost.Spirit.X3 の練習1

Boost.Spirit.X3 という C++ のための パーサコンビネータライブラリを使ってみています.
Boost.Spirit というと, C++ の黒魔術の塊みたいなイメージがあります. ちなみに Boost.Spirit.Qi が安定版のパーサコンビネータライブラリで, X3 はまだ開発中のようなので注意してください.

X3Qi と異なり,C++14 以降の規格を前提にしています.そのため,ジェネリックラムダなどを用いてよりわかりやすいプログラムが書けるようになっているようです.
Qiコンパイル時間が爆発していたのですが, X3 だと多少マシのようです.

第一歩

まずは猛烈にシンプルなパーサを使ってみましょう. X3Qi 同様に定義済みのパーサがあるので,それを単純に使用するだけのサンプルです.

#include <boost/spirit/home/x3.hpp>

#include <iostream>
#include <string>


int main()
{
  namespace x3 = boost::spirit::x3;

  int result;
  std::string src{ "123" };

  auto first = std::cbegin(src);
  auto const last = std::cend(src);
  bool success = x3::parse(first, last, x3::int_, result);

  if (!success || first != last) {
    // parse failed
    std::cerr << "Parse failed." << std::endl;
  } else {
    std::cout << "Parse: " << result << std::endl;
  }
}

コンパイル & 実行

$ clang++ -std=c++14 int_parser.cpp
$ ./a.out
Parse: 123

Boost.Spirit.X3 を使用する場合は, #include <boost/spirit/home/x3.hpp> とすればオッケーです.(これは使用していない機能のヘッダも読み込んでしまっているので,コンパイルは重くなります… が,これ以降使う機能を増やす度にヘッダを書き換えるのは面倒ですし,X3 の機能の多くを使用するプログラムの場合は,大差ないと思います.)

名前空間boost::spirit::x3 です.頻繁に出てくるので namespace x3エイリアスしました.

実際にパースしている部分は,

bool success = x3::parse(first, last, x3::int_, result);

の部分です.
x3::parse は,第一引数にソース文字列の先頭イテレータ,第二引数にソース文字列の終端イテレータをとります.
第三引数が,パーサの定義です.ここでは, x3::int_ という定義済みパーサを使用しました. これは, 整数を表す文字列を受けて,それを整数に変換するパーサです. たとえば "1"1 に変換します.整数以外にあたった場合はマッチせず,パースに失敗します.(x3::parse の返り値が false になる) 第四引数は,指定したパーサの返す値です.ここちょっと説明が難しいですね.
x3::int_int 型の値を返すパーサです(これを x3::int_ の attribute が int 型であると表現している?).
そこで,x3::int_ の返す値を格納する変数として,int result の参照を渡しているという感じです.
パースが成功していれば,result の中身は x3::int_ の返り値になっているはずです.

パースの成否判定は successfirst == last で行います.

if (!success || first != last) {

success はそもそもパーサにソースがマッチしなかった場合, false になります.
また,"123abc"x3::int_ をマッチさせると,"123" だけが消費され,"abc" が残ります.この時,first"a" の位置まで進んでいます. もしソースが先頭から終端まで,パーサにマッチしていたならば,first == last となるはずです.

というわけでこのプログラムは,x3::int_"123" をパースさせるプログラムでした.結果,きちんと整数の 123 が取得できていることがわかります.

コンビネータ

Boost.Spirit.X3 はパーサコンビネータライブラリです.個々のパーサを組み合わせてみましょう.

" ( 1234.5)" とか "(67.8 ) " にマッチして,double を返すようなパーサを定義してみます.

auto parser = x3::lit("(") >> x3::double_ >> x3::lit(")");
bool success = x3::phrase_parse(first, last, parser, x3::ascii::space, result);

まずは parser の定義です. x3::litリテラルを表します.引数にとった文字列にマッチし,何も返さないパーサです.x3::lit("(")( にマッチし,何も返さないパーサということになります.
x3::double_x3::int_ と同じく,定義済みパーサで,浮動小数点数にマッチしその値を返します.

そして重要なのが,>> です.
これは,「左辺にマッチした後,右辺にマッチするパーサ」を作り出すコンビネータです.
ここでは,x3::lit("(") >> x3::double_ ですから,( にマッチした後,浮動小数点数にマッチするパーサ,ということになります.

通常,>> は左辺の返す値と右辺の返す値のタプルを返します.(x3::int_ >> x3::double_ ならば,intdouble のタプル)
しかし,左辺右辺どちらか一方が値を返さない(正確には x3::unused_type を返す) 場合には,もう一方の値だけを返します.
つまり,x3::lit("(") >> x3::double_double だけを返し,>> x3::lit(")") と続けても double だけが返ります.

次に,空白の読み飛ばしについてです.

bool success = x3::phrase_parse(first, last, parser, x3::ascii::space, result);

ひとつ目の例と異なり,x3::parse ではなく x3::phrase_parse を使っています.
こちらは,attribute を示す最後の引数の前に,スキップパーサを取ります. スキップパーサとは,文字通り,スキップしてほしい文字列にマッチするパーサです.
x3::ascii::space は定義済みのパーサで,スペース,改行文字,タブにマッチします.したがって,これらの文字はスキップされます. スキップ判定のタイミングは >> の部分です.つまり,"12 3" は x3::int_ でパースすると, 12 までしかマッチしません.x3::int_ >> x3::int_ とすることで,スペースがスキップされます.

#include <boost/spirit/home/x3.hpp>

#include <iostream>
#include <string>


int main()
{
  namespace x3 = boost::spirit::x3;

  std::string src{ "( 123.4 )" };

  auto first = std::cbegin(src);
  auto const last = std::cend(src);
  double result;
  auto parser = x3::lit("(") >> x3::double_ >> x3::lit(")");
  bool success = x3::phrase_parse(first, last, parser, x3::ascii::space, result);

  if (!success || first != last) {
    // parse failed
    std::cerr << "Parse failed." << std::endl;
  } else {
    std::cout << "Parse: " << result << std::endl;
  }
}

ちなみに,x3::lit("(") >> x3::double_ >> x3::lit(")") の部分ですが,operator<< の引数の内,片方がパーサであれば,char const* から暗黙変換が働くので, "(" >> x3::double_ >> ")" と書くことが出来ます.

コンビネータは他にもあります.

  • |
    • a | b で,a にマッチするか b にマッチするか
    • a を先に試すので,両方にマッチする場合でも a にマッチしたことになる (PEG の特徴ですね)
    • 返り値は boost::variant<a, b> です.ab が同じ型を返す場合はその型を返します
  • >
    • >> とほぼ同じです.
    • a >> ba にマッチして b にマッチしなかった場合,a の前にバックトラックします.
    • a > b はその場合,即座にパース失敗を通知します.
  • *
    • *a という形で使う
    • a の 0 回以上の繰り返し
    • 返り値は std::vector<a>
  • +
    • +a という形で使う
    • * の一回以上繰り返し版
  • -
    • -a という形で使う
    • a が来ても来なくても良いというパーサ
    • 返り値は boost::optional<a>

...

>>> の違いはわかりにくいですね.
(x3::lit("(") >> ")") | ("(" >> x3::int_ >> ")")"(123)" を食わせると,| の後半部分にマッチしてくれます. 一方, >>> に置換えた場合,ソース先頭の"("x3::lit("(") にマッチするにもかかわらず,その直後に")" が来ていないため,その時点でエラーを通知してしまいます.

一旦むすび

とりあえず最も基本的な部分をまとめてみました.
ここまでは Qi と同じなんですよね.

セマンティックアクションや on_error などの扱いががっつり変わっているようなので,一旦ここで切って,それぞれ調べてからまとめたいと思います. 何か間違い等あればぜひご指摘お願いします.