如何解决 C++ 中针对 AST 上的访问者模式的标头重复问题
Posted
技术标签:
【中文标题】如何解决 C++ 中针对 AST 上的访问者模式的标头重复问题【英文标题】:how to solve header recurrence in C++ for visitor pattern over AST 【发布时间】:2012-10-19 00:41:21 【问题描述】:请注意:这些是描述一般困境的代码 sn-ps。完整的代码确实包括“包含警卫”/#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 的所有具体子类都存在。也就是说,例如,在 virtual void visit(StringElement* e)
中为签名声明 StringElement
但是在 ASTVisitor.h 中包含 ASTVisitor.h 和在 ASTVisitor.h 中包含 AST.h 会导致访问者类没有被 Node 类“看到”,因此,它作为接受参数的类型无效。此外,像 AST.h 中的 class Visitor;
这样的前向声明只能解决方法签名的类型问题,但在方法 v->visit(this)
内部仍然无效,因为前向声明没有说明访问者类的方法。
那么解决这个问题的 C++ 方法是什么?
【问题讨论】:
【参考方案1】:是的,在 C++ 中有一种方法可以做到这一点。您需要使用前向声明,如果需要,还需要拆分声明和定义。这是一个示例(请阅读 cmets 进行解释):
#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,否则您可能会多次包含同一个文件,这会导致很多错误。
【讨论】:
有包括警卫。为了问题的简洁,我只是没有复制它们。 @SaldaVonSchwartz:很好。我只是不确定你对 C++ 了解多少,所以以防万一。你知道... 您的解决方案仍然对我不起作用。如果我在访问者标头中转发声明所有具体节点类型,那么由于我需要在 AST.cpp 中同时包含访问者标头和 AST 标头以实现接受方法,因此我得到访问者和节点是模棱两可的,因为它们已被声明在 h 文件中,但也有对它们的前向声明(对 AST.h 中的访问者和 ASTVisitor.h 中的节点) 啊哈。执行此操作的 C++ 方法是不定义,只需声明您的接受方法。您在头文件中定义它,因此在处理头文件时需要知道这些方法。你会看到@Vlad 只声明了accept
- 甚至对此发表了有用的评论。创建一个定义为accept
的.cpp 文件,然后才需要知道v
有一个visit
函数。在你的 .cpp 文件中包含这两个标题,你应该没问题。
弗拉德,您的解决方案是对的! Xcode 刚刚开始成为... Xcode 在那个时候。我创建了一个新项目并导入了相同的 AST.h、AST.cpp 和 ASTVisitor.h,它编译得很好。前向声明的歧义和脱节消失了。我将文件重新导入到我的原始项目中,现在它们编译得很好【参考方案2】:
需要明确的是,这不是关于访问者模式的问题。更多的是关于递归包含问题...
首先,您应该确保在您的项目中使用单独的编译。也就是说,将接口放在 .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
这将阻止编译器尝试在单个编译单元中多次包含并重新定义该类。
从您上面的代码看起来,如果您不想使用预处理器,您可能可以只使用单独的编译和前向声明来获得优势。告诉我进展如何。
【讨论】:
我认为您可能还没有阅读全文。首先,我有包括警卫。我已经回答过了。其次,我知道前向声明。那不是问题。我最初的问题甚至解释了我尝试了前向声明。第三,我对前两次解决方案的尝试清楚地表明我也尝试将实现移动到 cpp 文件中,此时我得到了离线错误【参考方案3】:创建、编译和链接一个包含accept()
实现的文件AST.cpp:
void Node::accept(Visitor* v) v->visit(this);
现在,ASTVisitor.h 包含 AST.h,而 AST.h 声明 class Visitor;
。 CPP 文件包含这两个头文件。
为什么accept
声明为virtual
?
对于“真实”接口,在Visitor
的方法声明中将 替换为
= 0;
。
编辑:考虑您的实施,并在 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));
从NodeAcceptor<NodeSubclass>
派生每个NodeSubclass
。
这确保调用了正确的accept()
方法。
【讨论】:
是的,Node 类中的 virtual 是我尚未清理的先前实现的一部分,就像 Visitor 不是抽象的一样。只是因为我正在测试一些方法同时在访问者中,并希望能够实例化该类。但是这两件事最终都会为最终代码清理干净 我刚刚在包含 AST.h 和 ASTVisitor.h 的 AST.cpp 中尝试了您的方法,但我收到了两个错误:接受签名的定义不符合规定,并且 v- 没有匹配的成员函数>访问(这个)。我假设后者意味着即使在运行时从未使用过,我也需要在访问者中使用虚拟访问(Node*)方法,对吗?由于编译器在实现该方法时显然会根据泛型 Node 指针检查类型。但是前一个错误是怎么回事? 异常错误是标头中的accept声明为'void accept(Visitor* v);',而在类声明之前只有Visitor的前向声明 但每个班级都没有“正确接受”。它们都应该继承基本实现。 accept 只是为了让访问者根据它们的类型对节点子类做不同的事情。 但这不适用于您提供的代码。其他语言可能会根据this
所指对象的“真实”类型选择“正确的”visit
方法,但 C++ 不会——它会愚蠢地为所有子类型调用 visit(Node*)
。这就是“没有匹配成员”错误的原因,这就是 Vlad 为每个子类重新实现 accept
的原因,这就是我建议使用 CRTP 的原因。以上是关于如何解决 C++ 中针对 AST 上的访问者模式的标头重复问题的主要内容,如果未能解决你的问题,请参考以下文章
python3.3 上的烧瓶:“TypeError:AST 标识符必须是 str 类型”
本机如何直接 访问云服务器上的 virtualbox 虚拟机
在 Visual C++ 2005 中开发的 Visual C++ 项目 - 在 Visual C++ 2010 中,打开菜单时调试断言失败,但发布模式有效,如何解决?