web-dev-qa-db-ja.com

C ++再帰降下パーサー:グローバル変数ジレンマ

私は要点にまっすぐ行きます。私は趣味のプロジェクトのためにC++で再帰的降下パーサーを作成しようとしています。これには、独自のミニマリストプログラミング言語の作成が含まれます。

しかし、私を困惑させるのは、グローバル変数に関するジレンマです。明らかに再帰的降下では、コード全体を再帰的に解析する各文法パターンに複数の関数を実装する必要があります。ただし、インターネットで見たパーサーのほぼすべての例では、どの関数もパラメーターを取らず、何も返しません。

これは通常、グローバル変数を使用する必要性を示します。しかし、一般的にグローバル変数を使用することは悪い考えと考えられているので、それらを使用することについてはむしろ心配しています。

だから、私が知りたいのは:

この場合、グローバル変数を使用しても問題ありませんか?または、より良い代替案はありますか?

Wikipediaの再帰的降下パーサーのC実装 が現在のトークンも格納するグローバル変数を含んでいることを考えると、そのメソッドを使用する必要がありますか?

また、私が実装したtokenstreamクラスはLL(k)の先読みを許可していることも通知する必要があるため、現在のトークンだけをグローバル変数に格納するだけでは不十分な場合があります。

この質問が以前に尋ねられた場合は申し訳ありませんが、私は深刻なジレンマに陥っているため、真剣にあなたの助けを必要としています。

前もって感謝します。

私の結論:以下の回答で説明するように、この問題には複数の解決策があることに気付きました。私は議論の両側を聞いて、Parserクラスを作成するのが最良の選択肢であるという結論に達しました。その方法は、それが編成されるだけでなく、Parserの複数のインスタンスを同時に実行できるからです。

もう1つのオプションは、トークンストリームまたは現在のトークンをパラメーターとして文法関数に渡し、関数から値を返すことですが、コードが複雑になる可能性があるため、選択しませんでした。

(内部リンク用の)名前のない名前空間内でグローバル変数を使用するだけでも機能しますが、最初の2つの方法はより整理されています。

これは私が議論から理解したことであり、将来これを見る誰もが助けられるように私の選択を説明しました。

お役に立てば幸いです。

再びみんなに感謝

3
Famiu

問題の性質上、1つのパーサーに属するメソッドのグループは密に結合されています。パーサーはallこれらの関数間で共有される管理するいくつかの状態を持っています。したがって、それらの関数のローカルスコープ外の変数にその状態を入れることは、完全に理にかなっています。ある関数から別の関数に状態を渡すと、同じ最初のパラメーター "context"または "parsing_state"またはこのようなものを持つ20個以上の関数が生成されるだけで、グローバルに勝る利点はありません。

ただし、これを明確にするために、これをC++で「本当に」グローバル変数にすることはお勧めしません。 @ 1201ProgramAlarmのコメントで述べたように、パーサー全体をC++のクラスにカプセル化し、状態をそのクラスの1つ以上のメンバー変数として設計することは理にかなっています。したがって、これらの変数はパーサーのコンテキスト内でのみ「グローバル」であり、プログラム全体ではグローバルではありません。

10
Doc Brown

この場合、グローバル変数を使用しても問題ありませんか?

承知しました。これは趣味のプロジェクトです。つまり、保守性はそれほど優先度が高くありません。あなたの例はそれらを使用しているので、それらも使用すると、例はより理解しやすくなります。

または、より良い代替案はありますか?

とはいえ、グローバル変数を回避することは完全に正しいことであり、例から大きく逸脱したい場合は、あらゆる種類の問題が発生します。より良い代替策は、必要な場合はコンテキストとともに、解析する文字列(またはトークンシーケンス)を各関数に渡すことです。

理想的な世界では、解析された入力を表すある種の構文ツリーを返すだけです。ただし、一部の言語では、構文解析時にコンテキストまたは環境を変更する必要があります。そして、すべての言語はしないが正しく解析する入力を処理する必要があります。

一般的にはグローバルを避けることをお勧めしますが、プログラミング言語の設計を学びたいのであれば、例に従うだけの方が賢明かもしれません。これにより、最短で言語を実装できるため、目の前のタスクに集中できます。

1
Telastyn

グローバル変数を使用する理由は(もしあれば)あまりありません。パーサー関数は多くのパラメーターを渡すことはめったにありません。これは、通常、それらの間には実際の依存関係がないためです(つまり、あるパラメーターから別のパラメーターに渡す必要があまりないためです)。

少なくとも一般的に実装されているように、外界への唯一の依存関係は、どこかから入力を読み取る必要があるレクサーにあります。それ以外はすべて、必要なときにレクサーを使用してトークンを読み取るだけです。ソースをパラメーターとして(たとえば)レクサーオブジェクトのコンストラクターに渡します。これは単純な例ですが、最低限の程度で機能します。

#include <iostream>
#include <string>
#include <cctype>

class Lex {
    std::istream &is;
public:
    Lex(std::istream &is) : is(is) {}

    char token() { 
        char ch;
        is >> ch;
        return ch;
    }

    template <class T>
    Lex &operator>>(T &t) { is >> t; return *this; }

    void unget() { is.unget(); }
};

int expression(Lex &);

int factor(Lex &L) { 
    int val = 0;
    char ch = L.token();
    if (ch == '(') {
        val = expression(L);
        ch = L.token();
        if (ch != ')') {
            std::string error = std::string("Expected ')', got: ") + ch;
            throw std::runtime_error(error.c_str());
        }
    }
    else if (isdigit(ch)) {
        L.unget();
        L >> val;
    }
    else throw std::runtime_error("Unexpected character");
    return val;
}

int term(Lex &L) { 
    int ch;
    int val = factor(L);
    ch = L.token();
    if (ch == '*' || ch == '/') {
        int b = term(L);
        if (ch == '*')
            val *= b;
        else
            val /= b;
    }
    else L.unget();
    return val;
}

int expression(Lex &L) {
    int val = term(L);
    char ch = L.token();
    if (ch == '-' || ch=='+') {
        int b = expression(L);
        if (ch == '+')
            val += b;
        else
            val -= b;
    }
    else L.unget();
    return val;
}

int main(int argc, char **argv) {
    Lex L(std::cin);
    try {
        std::cout << expression(L);
    }
    catch(std::exception &e) {
        std::cerr << e.what();
    }
    return 0;
}
1
Jerry Coffin