サブルール

Spirit は式テンプレートを用いて実装されている。 これはとても強力なテクニックであるが、その力のためにいくつかの問題が持ち上がる。 ij および k がいずれも整数であるとき、 i | j >> k と書いたなら、その結果は整数のままであると予想するものだ。 だが式テンプレートを用いると、 ij および kT 型であるとき、 同じ式 i | j >> k の結果は複雑な合成型となる。[ Basic Concepts を参照] Spirit の式はプリミティブパーサとコンポジットパーサの組み合わせであるが、これは新たな型の無限の組み合わせを生む。 一つの問題は、複雑な型を生み出すような任意の複雑な式の型を推定する簡便な機能を C++ が提供していないことである。 そのため、次のように書くのは容易であるにも関わらず:

    int r = i | j >> k; // ここで i 、 j 、および k は int

ij および k が Spirit パーサである場合、 ルールのことをしばらく忘れたとしても、 以下のようなことをする簡単な方法は C++ には存在しない:

    <what_type> r = i | j >> k; // ここで i 、 j 、および k は Spirit パーサ
式テンプレートが型を無尽蔵に生み出してしまうからだ。

ij および k がすべて chlit<> オブジェクトであるとき、我々が望むのは次のものである:

    typedef
        alternative
        <
            chlit<>,        //  i
            sequence
            <
                chlit<>,    //  j
                chlit<>     //  k
            >
        >
    rule_t;

    rule_t r = i | j >> k;     // ここで i 、 j 、および k は chlit<> オブジェクト

理解できるように型宣言を意識的に読みやすく整形してある。 より複雑な式ではどうなるか試してみればいい。 たとえそれが出来たとしても、 Spirit の式テンプレートの型を明示的に綴ることは 面倒でエラーを誘発する。 右辺( rhs )は左辺( lhs )を反映しなければならない。

typeof と auto

コンパイラの中には既に tyoeof キーワードをサポートしているものがある。 これは型(type)を明示的に打ち込む(type)(意図的な語呂合わせだ)ことから我々を解放するために使われることができる。 typeof を用いると、上述の Spirit 式は以下のように書き直せる:

typeof(i | j >> k) r = i | j >> k;

これは複雑な型を明示的に宣言しなければならないことに比べればマシであるが、冗長でエラーを起こしやすく、それにまだ目が痛くなる。 式は二回も打ち込まれている。 これを単純化する唯一の方法はマクロを導入することだ

#define
RULE(NAME, DEFINITION) \
typeof((DEFINITION)) NAME =
(DEFINITION)

David Abrahams は comp.std.c++ で 名前付きテンポラリを導入するためにautoキーワードを再利用するよう提案した。 これは boost.org で大いに議論された。 例えば:

auto r = i | j >> k;

このような C++ の拡張が標準に受け入れられれば、これは冴えた解決法となり、我々の目的に上手く適合するだろう。 とは言え、これは完全な解決法ではない。 まだ事前に右辺を知らないという状況がありうるからだ; 例えば、循環依存するルールを先に宣言するときのように。

幸運なことに、ルールは救いの手をさしのべてくれる。ルールは、それに代入されたルールの型をキャプチャすることができる。 すなわち:

    rule<> r = i | j >> k;  // ここで i 、 j 、および k は chlit<> オブジェクト

表に出ることはないが、舞台裏では、元のルール(plain rule)は、実は代入されたパーサの動的型を保持する 実行時ポリモーフィックな抽象クラスへのポインタで実装されている。 Spirit の式がルールに代入されると、その型は抽象クラスの具象サブクラスにカプセル化される。 仮想関数 parse はカプセル化されたオブジェクトに構文解析を委譲する。

ただしルールにも弱点はある:

特定のスキャナ型と対になっている。 ルールは特定のスキャナと結びつけられている[スキャナ を参照]
ルールの parse メンバ関数は 仮想関数呼び出しのオーバーヘッドを持っており、インライン化できない

静的ルール:サブルール

サブルールは完全に静的なルールの一種であり、上述の欠点を持たない。

サブルールは特定のスキャナに結びつけられておらず、 そのためほとんどあらゆるスキャナ型を用いることができる
サブルールは仮想関数呼び出しを持たないので、 積極的なインライン化をも可能とする

    template<int ID, typename ContextT = parser_context>
    class subrule;

第一のテンプレートパラメータはサブルールに識別タグを与える。 rule と同様に ContextT テンプレートパラメータが存在し、 そのデフォルトは parser_context である。 サブルールの低レベルな振る舞いを調節するつもりがない限り、 ContextT テンプレートパラメータについて気にする必要はまったくない。 ContextT テンプレートパラメータの詳細な情報は別のところ で与えられる。

上記は公開された API である。 ContextT の後には実際にはより多くのテンプレートパラメータが存在しうる。 ContextT パラメータの後ろのすべてのものは利用者に考慮されるべきではなく、厳格に内部でのみ使用すべきものである。

いくつかの些細な違いを除けば、サブルールはルールに似た使い方と構文に従っている。 以下はサブルールを用いた電卓の文法である:

    struct calculator : public grammar<calculator>
    {
        template <typename ScannerT>
        struct definition
        {
            definition(calculator const& self)
            {
                first =
                (
                    expression  = term >> *(('+' >> term) | ('-' >> term)),
                    term        = factor >> *(('*' >> factor) | ('/' >> factor)),
                    factor      = integer | group,
                    group       = '(' >> expression >> ')'
                );
            }

            subrule<0>  expression;
            subrule<1>  term;
            subrule<2>  factor;
            subrule<3>  group;

            rule<ScannerT> first;
            rule<ScannerT> const&
            start() const { return first; }
        };
    };

セマンティックアクションを含む完全に動作する例は ここから参照することができる。これは Spirit 配布物の一部である。
[ libs/spirit/example/fundamental/calc/subrule_calc.cpp を参照]

サブルールは効率の良いルールの一種である。 積極的なインライン化のようなコンパイラの最適化は、コードサイズの削減とパフォーマンスの向上の顕著な助けとなる。

しかしサブルールは万能薬ではない。 そもそも、サブルールは C++ コンパイラをこき使う。 例えば現在のコンパイラは越えられない再帰の深さ制限がある。サブルールだけを用いて完全なパスカル文法を書こうなどと考えてはならない。 サブルールを用いた文法は単一の C++ の式となる。現在の C++ コンパイラは非常に複雑な式を上手く扱うことができない。 最後に、プレーンなルールはサブルールのプレースホルダーの役を果たすためにまだ必要である。

上記のコードは推薦されるサブルールの使用方法の良い例である。 階層構造に注目して欲しい。 電卓全体をカプセル化する文法がある。 スタートルールはサブルールの集合を保持するプレーンなルールである。 サブルールは順々に文法の実際の詳細を定義している。

テンプレートのインスタンス化の深さ

Spirit は C++ コンパイラをこき使う。現在の C++ コンパイラは非常に複雑で何重にも入れ子にされた式を上手く扱うことができない。 制限要因の一つは、典型的なコンパイラの再帰的テンプレートの深さ制限である。 すべてではないがいくつかのコンパイラではこの制限を調節できる。 g++ のデフォルトのテンプレートのインスタンス化制限は 17 である。 この上限はコンパイラフラグ -ftemplate-depth を用いて設定することができる。 もし比較的複雑な文法を持っているならばこの値を適切に設定するとよい。

実は、重要なテンプレートに関する再帰制限は二つある。 一つはテンプレートクラスのインスタンス化の深さである。 もう一つはテンプレート関数のインスタンス化の深さである。 いくつかのケースでは(例えば Borland )これらは独立している。 Borland はテンプレートクラスのインスタンス化の深さを変更不能な 257 という値に制限しているが、 テンプレート関数のインスタンス化の深さについては 1000 以上でも受け入れることができる。 しかし、リンカは内部リンカエラーで落ちる。これはおそらくリンク時に使用可能なメモリ量に依存している。

Microsoft Visual C++ はテンプレートクラスとテンプレート関数の両方のインスタンス化の深さに対して 1000 以上を取ることができる。 しかし Borland と同様、以下のプラグマを用いてインライン再帰の深さを設定しない限り、リンカは深いテンプレート関数のインスタンス化で落ちる:

#pragma inline_depth(255)
#pragma inline_recursion(on)

この設定は良いバランスを与える。 サブルールはすべての仕事をこなす。 それぞれの文法は一つのルール first のみを持つだろう。 ルールはただサブルールを保持するためだけに用いられ、それらを文法から見えるようにようにする。

文法の定義

ルールと同様、代入演算子 = の後の式はサブルールを定義する:

    identifier = expression

ルールとは異なり、サブルールは一度だけ定義される。 サブルールの再定義は不正であり、コンパイル時にアサーションを起こす。

セパレータ [ , ]

ルールがセミコロン ';' で終わるのに対して、サブルールはカンマ ',' で区切られる。 パスカルのステートメントと同じく、最後のグループのサブルールは後ろにカンマを持たない。

    a = ch_p('a'),
    b = ch_p('b'),
    c = ch_p('c'), // 駄目、カンマが付いている

    a = ch_p('a'),
    b = ch_p('b'),
    c = ch_p('c')  // OK

スタートサブルール

ルールとは異なり、構文解析はスタートサブルールから進行する。 グループの最初の(一番上の)サブルールはスタートサブルールと呼ばれる。 上の例では expression がスタートサブルールである。 サブルールのグループが呼び出されれば、スタートサブルール expression が最初に呼び出される。

それぞれのサブルールは対応する ID を持つ。 ID はサブルールを一意に特定する整数定数である。 上の例では四つのサブルールがある。それらは以下のように宣言されている:

    subrule<0>  expression;
    subrule<1>  term;
    subrule<2>  factor;
    subrule<3>  group;

別名

同じ ID のサブルールを複数持つことができる。 同じ ID のサブルールは一方の別名となる。 どちらのサブルールも相互に入れ替えて用いることができる。

    subrule<0>  a;
    subrule<0>  alias;  // a の別名

グループ:スコープと入れ子

サブルールとその定義のスコープはそれを囲んでいるグループであり、 普通は(慣例として)括弧の内側に囲まれている。 ID の外側のスコープを直接見ることはできない。 内部のサブルールグループは、別の括弧の組の内側に各サブグループを囲んで、入れ子にすることができる。 それぞれのグループは個々に独立して働く。 どちらのグループも互いに独立しているため、あまり賢明とは言えないが、あるグループのサブルールが別のグループのサブルールと同じ ID を共有することができる。

    subrule<0> a;
    subrule<1> b;
    subrule<0> c;
    subrule<1> d;

    (                       // 外側のサブルールグループ、 a と b のスコープ
        a = ch_p('a'),
        b =
        (                   // 内側のサブルールグループ、 b と c のスコープ
            c = ch_p('c'),
            d = ch_p('d')
        )
    )

サブルールの ID はあるグループの中でのみ一意である必要がある。 文法は暗黙のグループである。 さらに、ある文法の中のサブルール同士でさえ、もしそれらが同じグループの内側にあれば、衝突することなしに同じ ID を持つことができる。 サブルールは括弧を用いて暗黙にグループ化される。 括弧で囲まれたグループは一意なスコープを持つ。 上記のコードでは、外側のサブルールグループはサブルール a および b を定義する。 一方で内側のサブルールグループはサブルール c および d を定義する。 b の定義が内側のサブルールそのものであることに注意して欲しい。



このドキュメントの対象: Boost Version 1.30.0
最新版ドキュメント(英語)