如何使return类型的虚成员函数参数化
How to make the return type of a virtual member function parametric
我正在用 C++ 实现语法树的访问者模式。树中的每个节点都派生自基 class Expr
,它声明了一个纯虚方法 accept
。 accept
方法引用了 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 访问者模式(即具有 visit
和 accept
方法 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 的建议。
我正在用 C++ 实现语法树的访问者模式。树中的每个节点都派生自基 class Expr
,它声明了一个纯虚方法 accept
。 accept
方法引用了 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 访问者模式(即具有 visit
和 accept
方法 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 的建议。