如何使return类型的虚成员函数参数化

How to make the return type of a virtual member function parametric

我正在用 C++ 实现语法树的访问者模式。树中的每个节点都派生自基 class Expr,它声明了一个纯虚方法 acceptaccept 方法引用了 Visitor 的实例。访问者也是一个抽象 class 为语法树中的每种类型的节点声明一个 visit 方法。到目前为止一切顺利,classic 访问者模式。但是,我需要使 accept 的 return 类型参数化,但我还没有想出如何正确地做到这一点。因为 accept 是一个虚拟方法,所以我不能像这样使用模板:

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

我可以使用模板来声明 Expr class 本身,但是:

template <typename T>
class Binary;

template <typename T>
class Grouping;

template <typename T>
class Literal;

template <typename T>
class Unary;


template <typename T>
class Visitor
{
public:
    ~Visitor() = default;
    virtual T visitBinaryExpr(Binary<T> &expr) = 0;
    virtual T visitGroupingExpr(Grouping<T> &expr) = 0;
    virtual T visitLiteralExpr(Literal<T> &expr) = 0;
    virtual T visitUnaryExpr(Unary<T> &expr) = 0;
};

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

template <typename T>
class Binary : public Expr<T>
{
    std::shared_ptr<Expr<T>> left_;
    std::shared_ptr<Token> op_;
    std::shared_ptr<Expr<T>> right_;

public:
    Binary(std::shared_ptr<Expr<T>> left, std::shared_ptr<Token> op, std::shared_ptr<Expr<T>> right)
    {
        left_ = left;
        op_ = op;
        right_ = right;
    }

    T accept(Visitor<T> &visitor) override
    {
        return visitor.visitBinaryExpr(*this);
    }

    std::shared_ptr<Expr<T>> getLeft()
    {
        return left_;
    }

    std::shared_ptr<Token> getOp()
    {
        return op_;
    }

    std::shared_ptr<Expr<T>> getRight()
    {
        return right_;
    }
};

因为abstract classes不能被实例化,所以上面的好像行得通。但是,这个解决方案感觉不对(请记住我是 C++ 新手)。例如,如果我有一个 Binary 类型的节点,并且我想用两个具有不同 return 类型的函数访问它,我被迫为每个 return 类型都有一个对象实例空间化并在其中来回复制原始对象以获得 accept.

的适当行为

实施 accept 预期行为的更好方法是什么?

皮肤多态性的方法不止一种。

基于继承的多态性是您用来实现访问者模式的方法。我会说...不要。

来自这里:

virtual T visitBinaryExpr(Binary<T> &expr) = 0;
virtual T visitGroupingExpr(Grouping<T> &expr) = 0;
virtual T visitLiteralExpr(Literal<T> &expr) = 0;
virtual T visitUnaryExpr(Unary<T> &expr) = 0;

您已经有一组封闭的(或至少枚举的)类型。

使用std::variant.

template<class...Ts>
using PolyPtr=std::variant<std::shared_ptr<Ts...>>;
struct Binary;
struct Unary;
struct Literal;
struct Unary;
using ExprPtr=PolyPtr<Binary,Unary,Literal,Unary>;

现在您可以轻松访问

struct IsBinary{
  bool operator()(std::shared_ptr<Binary>)const{return true;}
  template<class T.
  bool operator()(std::shared_ptr<T>)const{return false;}
};
ExprPtr p=std::make_shared<Unary>();
std::cout<<std::visit(IsBinary(), p)<<'\n';

为此,表达式中的类型不需要相关。

如果您沉迷于标准多态性,您也可以在 Expr 中公开一个 virtual std::variant<A*,B*,C*> GetVariant()=0; 方法,并使用它来实现 accept

template<class V>
auto accept(Expr* expr, V&& v){
  return std::visit([&](auto* ptr){return v(*ptr);}, expr->GetVariant());
}

这假设访问者有一个 operator() 每个类型都有重载。

为了完整起见,此处描述了我发现的另一种解决此问题的方法:https://www.codeproject.com/Tips/1018315/Visitor-with-the-Return-Value。 这遵循 classic 访问者模式(即具有 visitaccept 方法 return void)。不同的 return 类型由单独的模板 class 和一些模板魔术处理,如下所示:

template <typename VisitorImpl, typename ResultType>
class ValueGetter
{
    ResultType value_;

public:
    static ResultType getValue(Expr &e)
    {
        VisitorImpl visitor;
        e.accept(visitor);
        return visitor.value_;
    }

    void returnValue(ResultType value)
    {
        value_ = value;
    }
};

class AstPrint : public Visitor, public ValueGetter<AstPrint, std::string>
{
public:
    void visitBinary(Binary &expr) override
    {
        std::string str;
        str += getValue(expr.getLeft());
        str += expr.getOp()->getLexeme();
        str += getValue(expr.getRight());
        returnValue(str);
    }
 
    /* ... */
};

AstPrint class 派生自基础模板 class ValueGetter 包含派生的 class 本身作为模板参数,允许实例化一个 Visitor 对于 getValue 访问的每个节点。这个模式叫做 Curious Recurrent Template Pattern(关于这个模式的更多信息可以在这里找到:https://www.fluentcpp.com/2017/05/12/curiously-recurring-template-pattern)。

visitBinary 通过将数据存储在属性 value_ 中来准备 return 数据。最后,当 visitBinary 完成对节点的访问后,数据由 getValue 编辑 return。下面是一个用例:

Expr *expression = new Binary(
        std::make_shared<Unary>(
            std::make_shared<Token>(TokenType::eMINUS, "-", nullptr, 1),
            std::make_shared<Literal>(
                std::make_shared<NumericLiteral>("123")
                )
            ),
        std::make_shared<Token>(TokenType::eSTAR, "*", nullptr, 1),
        std::make_shared<Grouping>(
            std::make_shared<Literal>(
                std::make_shared<NumericLiteral>("45.67")
            )
        )
    );

/* print -123 * (45.67) */
std::cout << AstPrint::getValue(*expression) << std::endl;

我认为这就是@Igor 的建议。