访客模式。 void* 是完全抽象接口的可接受返回类型吗?

Posted

技术标签:

【中文标题】访客模式。 void* 是完全抽象接口的可接受返回类型吗?【英文标题】:Visitor pattern. Is void* an acceptable return type for a completely abstract interface? 【发布时间】:2014-03-15 21:50:17 【问题描述】:

我有一个 AST,以通常的方式表示(抽象类型的节点树)。我有几个用于遍历这棵树的用例(一个优化器,它返回另一个 AST;IR 代码生成,它返回一个 llvm::Value*;和一个调试分析器,它只输出到标准输出并且什么都不返回)。

访问者感觉去这里是正确的方式,但是访问者的每个用例的不同返回类型使得很难看到如何为此实现接口。我考虑过这个:

class Visitor;

class ASTNode 
public:
  virtual void accept(Visitor *visitor);
;

class Visitor 
public:
  virtual void visit(CallNode *node) = 0;
  virtual void visit(BinExprNode *node) = 0;
  // etc
;

由于缺少返回值,每个访问者实现都需要建立内部状态并提供具有合适返回类型的result() 方法。然而,这很复杂,因为对visit() 的每次调用都需要围绕正在访问的表达式的一些上下文(即单独的调用,还是它被用作二进制表达式的一部分?)。对于二进制表达式代码生成之类的事情来说,通过访问操作数的节点来收集返回值变得很棘手(我可以使用内部临时状态变量或类似的东西来做到这一点,但感觉过度设计)并且难以推理(状态不断变化)。

class OptimizingVisitor : public Visitor 
  ASTNode *m_result;
public:
  ASTNode *result() 
    return m_result;
  

  void setResult(ASTNode *node) 
    m_result = node;
  

  void visit(CallNode *node) 
    node->left()->accept(this);
    ASTNode *optimizedL = result();
    node->right()->accept(this);
    ASTNode *optimizedR = result();
    setResult(new CallNode(optimizedL, optimizedR));
  

  // ... snip ...
;

我可以通过为每个节点传入一个新的访问者来使避免“状态不断变化”问题变得更加复杂。感觉这个问题需要一个更实用的解决方案。

真的,我想把上面写的更简单,更容易阅读:

class OptimizingVisitor : public Visitor 
public:
  ASTNode* visit(CallNode *node) 
    ASTNode *optimizedL = node->left()->accept(this);
    ASTNode *optimizedR = node->right()->accept(this);
    return new CallNode(optimizedL, optimizedR);
  

  // ... snip ...
;

当然,现在我已经更改了返回类型,所以它不符合我的访客合同。远非为访问者的每个可能实现定义完全独立的接口并使 AST 了解这些访问者类型,似乎唯一真正合乎逻辑的东西是void*。这种 API 是否保证使用void*?有没有更好的方法来做到这一点?

【问题讨论】:

能给我们一个模板吗? 我不确定。对于访客部分,是的。但是 AST 还需要遵守它的 accept() 方法。你能发布一个(简短的)例子吗? 好主意顺便说一句。这表明我缺乏 C++ 经验 :) 访客对我感觉不对。遍历树的目的是用附加信息来装饰它,以供以后使用,例如优化和代码生成。 不,我没有改变这棵树,我只想“看看它”。我通常会尽量避免不严格需要的可变接口。主观的,我知道。 【参考方案1】:

如果我做对了,我可能会有一些有用的东西。在https://gist.github.com/d11wtq/9575063 参考您的示例,您不能拥有

class ASTNode 
public:
   template <class T>
   virtual T accept(Visitor<T> *visitor);
;

因为没有模板虚函数。但是,您可能有一个泛型类

template <class D, class T>
struct host 
   virtual T accept(Visitor<T> * visitor);
   // I guess,  return visitor->visit(static_cast <D*>(this)); 
;

还有一个集合

template <class D, class... T>
struct hosts : host <D, T>...  ;

因此,当可能的返回类型集有限时,您可以说例如

class ASTNode : public hosts <ASTNode, T1, T2, T3> 
public:
   // ...
;

现在您拥有三种不同的返回类型 T1,T2,T3 的访客合同。

【讨论】:

嘿,很抱歉回复晚了——我不得不出去玩一天。作为一个 C++ 新手,这看起来像一个语法丛林 :) 为什么你使用结构而不是类?我会玩这个,看看感觉如何。我更倾向于保持简单,但现在只使用 void* (或对象)。 我完成了 setResult()result() 选项,但将其包裹在外观后面以隐藏该实现。感觉所有的选项都很丑。在这种情况下,动态类型会获得 +1。 有趣的是这里的所有语法,使用struct 是你最大的困难:-) 不管怎样,记住structclass 是一样的,除了struct 有@987654333 @继承和public默认成员,而class默认有private。您甚至可以将某些内容声明为struct,然后稍后将其定义或专门化为class。因此,您为每种情况选择什么只是为了方便。 @d11wtq 建议:void* 主要用于低级序列化,例如分配内存或文件 i/o。否则,当其他一切都失败时,将其作为最后的手段。现在看起来很简单的事情在以后变成一场噩梦,对你来说,或者(更糟)对别人来说。找出您的返回类型的模式。如果只有一组有限的可能类型,我的解决方案很容易应用。如果您有一个具有 2-3 种不同返回类型的 minimal 工作示例,我很乐意为您转换它。 @d11wtq (cont'd) 如果返回类型与输入类型相同,请按照拨号器的解决方案。如果是输入的函数(type函数),我认为这个解决方案也可以调整。如果有几十个访问者的返回类型是任意的,没有特定的模式,并且有几十个不同的返回类型,那么恐怕你就有麻烦了。【参考方案2】:

也许这会有所帮助。

使用返回类型协方差使accept() 调用类型安全:

class Visitor;

class ASTNode

public:
    virtual ASTNode* accept(Visitor* visitor);
;

class CallNode;
class BinaryExpressionNode;

class Visitor

public:
    virtual CallNode* visit(CallNode* node) = 0;
    virtual BinaryExpressionNode* visit(BinaryExpressionNode* node) = 0;
;

实现:

class CallNode : public ASTNode

public:
    CallNode(BinaryExpressionNode* left, BinaryExpressionNode* right);

    // return type covariance
    CallNode* accept(Visitor* visitor) override
    
        return visitor->visit(this);
    

    BinaryExpressionNode* left();
    BinaryExpressionNode* right();
;

class BinaryExpressionNode : public ASTNode

public:
    BinaryExpressionNode* accept(Visitor* visitor) override
    
        return visitor->visit(this);
    


class OptimizingVisitor : public Visitor

public:
    CallNode* visit(CallNode* node) override
    
        // return type covariance will make this operation type-safe:
        BinaryExpressionNode* left = node->left()->accept(this);
        auto right = node->right()->accept(this);
        return new CallNode(left, right);
    

    BinaryExpressionNode* visit(BinaryExpressionNode* node) override
    
        return new BinaryExpressionNode(node);
    
;

【讨论】:

不错!当然,假设协方差是应用程序所需要的。 嘿,谢谢大家。这非常好,虽然它只支持返回 ASTNode*(或其派生词)的类型。我还传递了一个生成 llvm::Value* 对象的访问者,这些对象来自完全不同的祖先。 @d11wtq 自从你提出这个问题以来,我已经考虑了很多,而且我也试图解决这个问题(在 arbitrary 多态之间转换以 100% 类型安全的方式绘制对象图),但收效甚微。它归结为让访问者具有与visit 方法一样多的泛型类型参数,或者返回void*(其C# 等效object 用于其他代码库,例如SharpDevelop AST)。虽然我还没有考虑完,可能会在稍后通过更深入的分析来编辑这个答案。

以上是关于访客模式。 void* 是完全抽象接口的可接受返回类型吗?的主要内容,如果未能解决你的问题,请参考以下文章

(嵌套?)多次调度 [访客模式]

带有内部树的访客设计模式

设计模式----抽象工厂模式

设计模式--抽象工厂模式

设计模式之-抽象工厂模式

转发Java设计当中的工厂设计模式