如何解决 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&lt;NodeSubclass&gt; 派生每个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 上的访问者模式的标头重复问题的主要内容,如果未能解决你的问题,请参考以下文章

如何在 OSX 上的 C++ 应用程序中播放合成器声音?

python3.3 上的烧瓶:“TypeError:AST 标识符必须是 str 类型”

如何构建针对 64 位环境的解决方案? [关闭]

C++:如何解决在未知点引起的第一次机会异常?

本机如何直接 访问云服务器上的 virtualbox 虚拟机

在 Visual C++ 2005 中开发的 Visual C++ 项目 - 在 Visual C++ 2010 中,打开菜单时调试断言失败,但发布模式有效,如何解决?