请注意:这些是描述一般困境的代码片段。完整的代码包括“包括警卫”/ #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)
但是AST.hisitor.h中的ASTVisitor.h和ASTVisitor.h中的AST.h导致Visitor类没有被Node类“看到”,因此不能作为accept参数的类型有效。此外,执行前向声明,如AST.h中的class Visitor;
只能解决方法签名的类型问题,但方法v->visit(this)
内部仍然无效,因为前向声明没有说明方法的方法访客班。
那么解决这个问题的C ++方法是什么?
答案 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
这将阻止编译器尝试在单个编译单元中多次包含并重新定义类。
从上面代码的外观来看,如果您不想使用预处理器,则可以使用单独的编译和转发声明。让我知道它是怎么回事。