【问题标题】:how to solve header recurrence in C++ for visitor pattern over AST如何解决 C++ 中针对 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++ 方法是什么?

【问题讨论】:

    标签: c++ header


    【解决方案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 的原因。
    猜你喜欢
    • 1970-01-01
    • 2012-11-07
    • 2015-09-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-04-15
    相关资源
    最近更新 更多