What's the best way to implement AST using visitor pattern with return value?

自作多情 提交于 2019-12-11 10:36:45

问题


I'm trying to implement a simple abstract syntax tree (AST) in C++ using the visitor pattern. Usually a visitor pattern does not handle return value. But in my AST there are expressions nodes which care about the return type and value of its children node. For example, I have a Node structure like this:

class AstNode
{
public:
    virtual void accept(AstNodeVisitor&) = 0;

    void addChild(AstNode* child);
    AstNode* left() { return m_left; }
    AstNode* right() { return m_right; }
...

private:
    AstNode* m_left;
    AstNode* m_right;
};

class CompareNode : public AstNode
{
public:
    virtual void accept(AstNodeVisitor& v)
    {
        v->visitCompareNode(this);
    }

    bool eval(bool lhs, bool rhs) const
    {
        return lhs && rhs;
    }
};

class SumNode : public AstNode
{
public:
    virtual void accept(AstNodeVisitor& v)
    {
        v->visitSumNode(this);
    }

    int eval(int lhs, int rhs) const
    {
        return lhs + rhs;
    }
};

class AstNodeVisitor
{
public:
    ...
    bool visitCompareNode(CompareNode& node)
    {
        // won't work, because accept return void!
        bool lhs = node.left()->accept(*this);
        bool rhs = node.right()->accept(*this);
        return node.eval(lhs, rhs);
    }

    int visitSumNode(Node& node)
    {
        // won't work, because accept return void!
        int lhs = node.left()->accept(*this);
        int rhs = node.right()->accept(*this);
        return node.eval(lhs, rhs);
    }
};

In this case both CompareNode and SumNode are binary operators but they rely on the return type of their children's visit.

As far as I can see to make it work, there are only 2 options:

  1. accept can still return void, save the return value in a context object which is passed to each accept and visit function, and use them in the visit function, where I know what type to use. This should work but feels like a hack.

  2. make AstNode a template, and accept function a none virtual, but return type depends on template parameter T.But if I do this, I no longer have a common AstNode* class and can't save any AstNode* in the children list.

for example:

template <typename T`>
class AstNode
{
public:
    T accept(AstNodeVisitor&);
    ...
};

So is there a more elegant way to do this? This should be a fairly common problem for people implementing AST walking so I'd like to know what's the best practice.

Thanks.


回答1:


The Visitor can have member that it can use to store result, something like:

class AstNodeVisitor
{
public:
    void visitCompareNode(CompareNode& node)
    {
        node.left()->accept(*this); // modify b
        bool lhs = b;
        node.right()->accept(*this); // modify b
        bool rhs = b;
        b = node.eval(lhs, rhs);
    }

    void visitSumNode(Node& node)
    {
        node.left()->accept(*this); // modify n
        int lhs = n;
        node.right()->accept(*this);  // modify n
        int rhs = n;
        n = node.eval(lhs, rhs);
    }
private:
    bool b;
    int n;
};

You may also want to save the type of last result or use something like boost::variant.




回答2:


template<class T> struct tag { using type=T; };
template<class...Ts> struct types { using type=types; }

template<class T>
struct AstVisitable {
  virtual boost::optional<T> accept( tag<T>, AstNodeVisitor&v ) = 0;
  virtual ~AstVisitable() {};
};
template<>
struct AstVisitable<void> {
  virtual void accept( tag<void>, AstNodeVisitor&v ) = 0;
  virtual ~AstVisitable() {};
};
template<class Types>
struct AstVisitables;
template<>
struct AstVisibables<types<>> {
  virtual ~AstVisitables() {};
};
template<class T0, class...Ts>
struct AstVisitables<types<T0, Ts...>>:
  virtual AstVisitable<T0>,
  AstVisitables<types<Ts...>>
{
  using AstVisitable<T0>::accept;
  using AstVisitables<types<Ts...>>::accept;
};

using supported_ast_return_types = types<int, bool, std::string, void>;

class AstNode:public AstVisitables<supported_ast_return_types> {
public:
  void addChild(AstNode* child);
  AstNode* left() { return m_left.get(); }
  AstNode* right() { return m_right.get(); }

private:
  std::unique_ptr<AstNode> m_left;
  std::unique_ptr<AstNode> m_right;
};

template<class types>
struct AstVisiablesFailAll;
template<>
struct AstVisiablesFailAll<> {
  virtual ~AstVisiablesFailAll() {};
};
template<class T>
struct AstVisitableFailure : virtual AstVisitable<T> {
  boost::optional<T> accept( tag<T>, AstNodeVisitor& ) override {
    return {};
  }
};
template<>
struct AstVisitableFailure<void> : virtual AstVisitable<void> {
  void accept( tag<void>, AstNodeVisitor& ) override {
    return;
  }
};
template<class T0, class...Ts>
struct AstVisitablesFailAll<types<T0, Ts...>>:
  AstVisitableFailure<T0>,
  AstVisitableFailAll<types<Ts...>>
{
  using AstVisitableFailure<T0>::accept;
  using AstVisitableFailAll<types<Ts...>>::accept;
};

So now you can boost::optional<bool> lhs = node.left()->accept( tag<bool>, *this );, and from the state of lhs know if the left node can be evaluated in a bool context.

SumNode looks like this:

class SumNode :
  public AstNode,
  AstVisiablesFailAll<supported_ast_return_types>
{
public:
  void accept(tag<void>, AstNodeVisitor& v) override
  {
    accept(tag<int>, v );
  }
  boost::optional<int> accept(tag<int>, AstNodeVisitor& v) override
  {
    return v->visitSumNode(this);
  }
  int eval(int lhs, int rhs) const {
    return lhs + rhs;
  }
};

and visitSumNode:

boost::optional<int> visitSumNode(Node& node) {
    // won't work, because accept return void!
    boost::optional<int> lhs = node.left()->accept(tag<int>, *this);
    boost::optional<int> rhs = node.right()->accept(tag<int>, *this);
    if (!lhs || !rhs) return {};
    return node.eval(*lhs, *rhs);
}

The above assumes that visiting a+b in a void context is acceptable (like in C/C++). If it isn't, then you need a means for void visit to "fail to produce a void".

In short, accepting requires context, which also determines what type you expect. Failure is an empty optional.

The above uses boost::optional -- std::experimental::optional would also work, or you can roll your own, or you can define a poor man's optional:

template<class T>
struct poor_optional {
  bool empty = true;
  T t;
  explicit operator bool() const { return !empty; }
  bool operator!() const { return !*this; }
  T& operator*() { return t; }
  T const& operator*() const { return t; }
  // 9 default special member functions:
  poor_optional() = default;
  poor_optional(poor_optional const&)=default;
  poor_optional(poor_optional const&&)=default;
  poor_optional(poor_optional &&)=default;
  poor_optional(poor_optional &)=default;
  poor_optional& operator=(poor_optional const&)=default;
  poor_optional& operator=(poor_optional const&&)=default;
  poor_optional& operator=(poor_optional &&)=default;
  poor_optional& operator=(poor_optional &)=default;
  template<class...Ts>
  void emplace(Ts&&...ts) {
    t = {std::forward<Ts>(ts)...};
    empty = false;
  }
  template<class...Ts>
  poor_optional( Ts&&... ts ):empty(false), t(std::forward<Ts>(ts)...) {}
};

which sucks, because it constructs a T even if not needed, but it should sort of work.




回答3:


For completion sake I post the template version that is mentioned by the OP

#include <string>
#include <iostream>

namespace bodhi
{
    template<typename T> class Beignet;
    template<typename T> class Cruller;

    template<typename T> class IPastryVisitor
    {
    public:
        virtual T visitBeignet(Beignet<T>& beignet) = 0;
        virtual T visitCruller(Cruller<T>& cruller) = 0;
    };

    template<typename T> class Pastry
    {
    public:
        virtual T accept(IPastryVisitor<T>& visitor) = 0;
    };

    template<typename T> class Beignet : public Pastry<T>
    {
    public:
        T accept(IPastryVisitor<T>& visitor)
        {
            return visitor.visitBeignet(*this);
        }

        std::string name = "Beignet";
    };

    template<typename T> class Cruller : public Pastry<T>
    {
    public:
        T accept(IPastryVisitor<T>& visitor)
        {
            return visitor.visitCruller(*this);
        }

        std::string name = "Cruller";
    };


    class Confectioner : public IPastryVisitor<std::string>
    {
    public:
        virtual std::string visitBeignet(Beignet<std::string>& beignet) override
        {
            return "I just visited: " + beignet.name;
        }

        virtual std::string visitCruller(Cruller<std::string>& cruller) override
        {
            return "I just visited: " + cruller.name;
        }
    };
}

int main()
{
    bodhi::Confectioner pastryChef;

    bodhi::Beignet<std::string> beignet;
    std::cout << beignet.accept(pastryChef) << "\n";

    bodhi::Cruller<std::string> cruller;
    std::cout << cruller.accept(pastryChef) << "\n";
    return 0;
}

Every pastry is a node and every visitor can implement its accepted return type. Having multiple visitor could visit the same pastry.



来源:https://stackoverflow.com/questions/28240936/whats-the-best-way-to-implement-ast-using-visitor-pattern-with-return-value

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!