7

注意してください: これらは、一般的なジレンマを表すコード スニペットです。完全なコードには、「ガードを含める」/ #pragma once / whathaveyou が含まれています。

私は AST をトラバースするためのビジター パターンを実装しており、次の問題を解決する C++ の方法を考えています。

ベース AST ノード クラス宣言を持つ AST.h があります。

    class Node
    {
    public:
        virtual void accept(Visitor* v) {v->visit(this);}
    };

宣言、式などのすべての具象ノード サブクラスとともに.

そして、次の行に沿って、ビジター インターフェイスを宣言する ASTVisitor.h があります。

    class Visitor
    {
    public:
        Visitor() {}
        virtual ~Visitor() {}

        virtual void visit(StringElement* e) {}
        virtual void visit(RealElement* e) {}
        virtual void visit(IntegerElement* e) {}
        ...

問題は、AST.h が ASTVisitor.h を必要とするため、Accept メソッドは Visitor オブジェクトに Visit メソッドがあることを認識します。つまり、 Visitor と visit() の両方が に対して宣言されるようになりvirtual void accept(Visitor* v) {v->visit(this);}ます。しかし同時に、Node の具象サブクラスがすべて存在することを Visitor クラスが認識できるように、ASTVisitor.h には AST.h が必要です。つまり、たとえば、 StringElement が署名に対して宣言されているようにvirtual void visit(StringElement* e)

しかし、ASTVisitor.h に ASTVisitor.h を含め、ASTVisitor.h に AST.h を含めると、Visitor クラスが Node クラスから「認識」されないため、accept のパラメーターの型として有効ではなくなります。また、class Visitor;AST.h のように前方宣言を行うと、メソッド シグネチャの型の問題のみが解決されv->visit(this)ますが、前方宣言は Visitor クラスのメソッドについて何も述べていないため、メソッド内は依然として無効です。

では、これを解決する C++ の方法は何ですか?

4

3 に答える 3

9

はい、C++ でこれを行う方法があります。前方宣言を使用する必要があり、必要に応じて宣言と定義を分割する必要があります。以下に例を示します (説明についてはコメントをお読みください)。

#include <cstdio>
#include <string>

/// --- A.hpp ---

// First, you have to forward declare a visitor type.
class Visitor;

// Then declare/define a node base class (interface).
class Node {
  public:
    Node() {}
    virtual ~Node() {}

    // Note that Visitor, as a type, is referenced here, but none of its
    // "body" is used, so forward declaration is enough for us.
    virtual void accept(Visitor & v) = 0;
};

/// --- B.hpp (includes A.hpp) ---

// Then, to declare the actual interface for a visitor, we must play the same
// trick with forward declaration, but for specific node types:
class NodeA;
class NodeB;

// And once those types are "pre-declared", declare visitor interface.
class Visitor {
  public:
    Visitor() {}
    virtual ~Visitor() {}

    virtual void visit(const Node & node);
    virtual void visit(const NodeA & node);
    virtual void visit(const NodeB & node);
};

/// --- C.hpp (includes B.hpp) ---

// Once visitor is declared, declare/define specific nodes.
class NodeA : public Node {
  public:
    std::string node_name;

    NodeA() : node_name("I am a node of type A!") {}
    virtual ~NodeA() {}
    virtual void accept(Visitor & v) { v.visit(*this); }
};

class NodeB : public Node {
  public:
    std::string node_name;

    NodeB() : node_name("B node here!") {}
    virtual ~NodeB() {}
    virtual void accept(Visitor & v) { v.visit(*this); }
};

// --- B.cpp (includes B.hpp and C.hpp) ---

// Now, nodes are declared, so that we can define visitor's methods.
// Note that if you don't need to use "node" parameters, this can
// as well go with declaration and there is no need to "define" this later.
void Visitor::visit(const Node & node) {
    printf("Base visitor got base node\n");
}

void Visitor::visit(const NodeA & node) {
    printf("Base visitor got node A\n");
}

void Visitor::visit(const NodeB & node) {
    printf("Base visitor got node B\n");
}

// --- YourProgram.[cpp|hpp] includes at most C.hpp --

// Than, at any point in your program, you can have a specific visitor:
class MyVisitor : public Visitor {
  public:
    MyVisitor() {}
    virtual ~MyVisitor() {}

    virtual void visit(const Node & node) {
        printf("Got base node...\n");
    }

    virtual void visit(const NodeA & node) {
        printf("Got %s\n", node.node_name.c_str());
    }

    virtual void visit(const NodeB & node) {
        printf("Got %s\n", node.node_name.c_str());
    }
};

// And everything can be used like this, for example:
int main()
{
    Visitor generic_visitor;
    MyVisitor my_visitor;

    NodeA().accept(generic_visitor);
    NodeA().accept(my_visitor);
    NodeB().accept(generic_visitor);
    NodeB().accept(my_visitor);
}

...ところで、インクルードガードを使用することを忘れないでください。そうしないと、同じファイルを複数回インクルードすることになり、多くのエラーが発生する可能性があります。

于 2012-10-19T01:03:09.373 に答える
1

明確にするために、これは訪問者パターンに関する質問ではありません。それは再帰的なインクルードの問題についてです...

まず、プロジェクトで個別のコンパイルを採用していることを確認する必要があります。つまり、インターフェイスを .h ファイルに配置し、実装を .cpp ファイルに配置します。これが事実であるかどうかはあなたの質問からはあまり明確ではありませんが、 Node::accept() の実装はヘッダー IMO にあるべきではありません。

前方宣言

個別のコンパイルが採用されている場合は、前方宣言を利用できます。コンパイラがそれらの型のインターフェイスを認識する必要のないヘッダー ファイルで参照される型は、ヘッダーの先頭で簡単に宣言できます。したがって、たとえば、AST.h に ASTVisitor.h を含める必要はありません。次のようにするだけです (ここでも、accept() の実装を cpp (AST.cpp) ファイルに移動したと仮定します)。

class Visitor;
class Node
{
public:
    virtual void accept(Visitor* v);
};

コンパイラは Visitor クラスについて何も知る必要がないため、これが機能することに注意してください。ポインター (Visitor*) としてのみ参照されるため、コンパイラーはインターフェースまたは実装 (メモリーフットプリント) を知る必要はありません。

プリプロセッサ。

accept() の実装をヘッダー ファイルに残したい場合は、プリプロセッサ アプローチを使用できます。とにかく、これを良い習慣として常にお勧めします。すべてのヘッダー ファイルを #ifndef ブロックでラップします。たとえば、AST.h (私はまだそこに前方宣言を持っています):

#ifndef ast_h
#define ast_h

class Visitor;

class Node
{
    ...
}

#endif //ast_h

そしてASTVisitor.hでも

#ifndef astvisitor_h
#define astvisitor_h

class StringElement;
class RealElement;
class IntegerElement;

class Visitor
{
    ...
}

#endif //astvisitor_h

これにより、コンパイラが単一のコンパイル単位にクラスを複数回含めて再定義しようとするのを防ぐことができます。

上記のコードの外観から、プリプロセッサを使用したくない場合は、おそらく個別のコンパイルと前方宣言を使用することができます。それがどうなるか教えてください。

于 2012-10-19T01:22:37.080 に答える
0

の実装を含むファイル AST.cpp を作成、コンパイル、およびリンクしますaccept()

void Node::accept(Visitor* v) {v->visit(this);}

現在、ASTVisitor.h には AST.h が含まれており、AST.h は を宣言してclass Visitor;います。CPP ファイルには、両方のヘッダー ファイルが含まれています。

acceptとして宣言されているのはなぜvirtualですか?

「真の」インターフェースの場合は、のメソッドの宣言で に{}置き換えます。= 0;Visitor

編集:あなたの実装について考え、Vladの答えの助けを借りて、何が問題なのかがわかります。の実装を繰り返さないようにするには、「奇妙な繰り返しテンプレート」パターンを使用しますaccept

class Node {
    void accept(Visitor* v) = 0;
}

template <class ME>
class NodeAcceptor : public Node {
    void accept(Visitor* v);
}

template <class ME>
void NodeAcceptor<ME>::accept(Visitor* v) { v->accept(static_cast<ME*>(this)); }

NodeSubclassからそれぞれ導出しNodeAcceptor<NodeSubclass>ます。

これにより、正しいaccept()メソッドが呼び出されることが保証されます。

于 2012-10-19T00:45:45.327 に答える