7

请注意:这些是描述一般困境的代码片段。完整的代码确实包括“包括警卫”/#pragma once/whathaveyou。

我正在实现用于遍历 AST 的访问者模式,并想知道解决以下问题的 C++ 方法是什么:

我有 AST.h,它具有基本的 AST 节点类声明:

    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 以便接受方法知道访问者对象有访问方法。也就是说,Visitor 和 visit() 都被声明为virtual void accept(Visitor* v) {v->visit(this);}. 但与此同时,ASTVisitor.h 需要 AST.h 以便 Visitor 类知道 Node 的所有具体子类都存在。也就是说,例如,StringElement 被声明为virtual void visit(StringElement* e)

但是,在 AST.h 中包含 ASTVisitor.h 和在 ASTVisitor.h 中包含 AST.h 会导致访问者类没有被 Node 类“看到”,因此,它作为接受参数的类型无效。此外,像class Visitor;在 AST.h 中那样进行前向声明只能解决方法签名的类型问题,但在方法内部v->visit(this)仍然无效,因为前向声明没有说明访问者类的方法。

那么解决这个问题的 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

对于“真正的”接口,在's 方法的声明中替换{}为。= 0;Visitor

编辑:考虑您的实施,并在弗拉德的回答的帮助下,我现在知道出了什么问题。使用“奇怪的重复模板”模式来避免重复执行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)); }

每个NodeSubclassNodeAcceptor<NodeSubclass>.

这确保accept()调用了正确的方法。

于 2012-10-19T00:45:45.327 回答