如何解决C ++中针对AST的访问者模式的头重现

时间:2012-10-19 00:41:22

标签: c++ header

请注意:这些是描述一般困境的代码片段。完整的代码包括“包括警卫”/ #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,以便accept方法知道Visitor对象有访问方法。也就是说,为virtual void accept(Visitor* v) {v->visit(this);}声明了Visitor和visit()。但与此同时,ASTVisitor.h需要AST.h,以便Visitor类知道Node的所有具体子类都存在。也就是说,例如,为virtual void visit(StringElement* e)

中的签名声明了StringElement

但是AST.hisitor.h中的ASTVisitor.h和ASTVisitor.h中的AST.h导致Visitor类没有被Node类“看到”,因此不能作为accept参数的类型有效。此外,执行前向声明,如AST.h中的class Visitor;只能解决方法签名的类型问题,但方法v->visit(this)内部仍然无效,因为前向声明没有说明方法的方法访客班。

那么解决这个问题的C ++方法是什么?

3 个答案:

答案 0 :(得分:4)

是的,有一种方法可以在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);
}

...顺便说一句,不要忘记使用include guards,否则你可能会多次包含同一个文件,这会导致很多错误。

答案 1 :(得分:0)

创建,编译和链接包含accept()的实现的文件AST.cpp:

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

现在,ASTVisitor.h包含AST.h,AST.h声明class Visitor;。 CPP文件包含两个头文件。

为什么accept声明为virtual

对于“真实”界面,请在{}方法的声明中将= 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)); }

NodeSubclass中获取每个NodeAcceptor<NodeSubclass>

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

答案 2 :(得分:0)

要明确的是,这不是关于访客模式的问题。它更多的是关于递归包含问题...

首先,您应该确保在项目中使用单独的编译。也就是说,将接口放在.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

这将阻止编译器尝试在单个编译单元中多次包含并重新定义类。

从上面代码的外观来看,如果您不想使用预处理器,则可以使用单独的编译和转发声明。让我知道它是怎么回事。