我正在尝试编写一个可重用的解析库(为了好玩)。
我写了一个Lexer
类,它生成一个Tokens
序列。 Token
是子类层次结构的基类,每个子类代表不同的令牌类型,具有自己的特定属性。例如,有一个子类LiteralNumber
(从Literal
派生而来自Token
),它有自己的特定方法来处理其词汇的数值。一般处理词位的方法(检索它们的字符串表示,源中的位置等)在基类Token
中,因为它们对所有标记类型都是通用的。此类层次结构的用户可以为我未预测的特定令牌类型派生自己的类。
现在我有一个Parser
类,它读取标记流并尝试将它们与其语法定义匹配。例如,它有一个方法matchExpression
,后者又调用matchTerm
,然后调用matchFactor
,它必须测试当前令牌是Literal
还是{{1} (都来自Name
基类)。
问题是:
我现在需要检查流中当前令牌的类型以及它是否与语法匹配。如果没有,抛出Token
例外。如果是,则相应地采取行动以在表达式中获取其值,生成机器代码,或者在语法匹配时执行解析器需要执行的任何操作。
但是我已经阅读了很多关于在运行时检查类型并从中确定是 bad design™的内容,并且它应该被重构为多态虚拟方法。当然,我同意这一点。
所以我的第一次尝试是在EParseError
基类中放置一些type
虚拟方法,这将被派生类重写并返回一些类型为id的Token
。
但是我已经看到了这种方法的缺点:从enum
派生他们自己的令牌类的用户将无法在库源中的Token
添加额外的id! : - /目标是允许他们在需要时扩展新类型令牌的层次结构。
我还可以从enum
方法返回一些string
,这样可以轻松定义新类型。
但是,在这两种情况下,基本类型的信息都会丢失(仅从type
方法返回叶类型),而type
类将无法检测到{{ 1}}派生类型,当有人从中派生并覆盖Parser
以返回Literal
以外的其他内容。
当然,type
类也适用于用户扩展(即编写自己的解析器,识别自己的令牌和语法),但不知道{{1}的后代是什么} class将来会在那里。
许多有关设计的常见问题解答和书籍建议在此场景中采用需要按类型决定的代码中的行为,并将其放入派生类中的虚拟方法覆盖中。但是我无法想象如何将这种行为放入"Literal"
后代中,因为例如,生成机器代码或计算表达式不是他们的业务。此外,语法的某些部分需要匹配多个令牌,因此没有一个特定的令牌可以将该行为放入其中。这是特定语法规则的责任,它可以匹配多个令牌作为终端符号。
如何改进此设计的任何想法?
答案 0 :(得分:3)
所有主要的C ++编译器都支持RTTI。这至少包括GCC,Intel和MSVC。可移植性问题确实已成为过去。
如果这是你不喜欢的语法,那么这里是一个很好的解决方案来实现RTTI:
class Base {
public:
// Shared virtual functions
// ...
template <typename T>
T *instance() {return dynamic_cast<T *>(this);}
};
class Derived : public Base {
// ...
};
// Somewhere in your code
Base *x = f();
if (x->instance<Derived>()) ;// Do something
// or
Derived *d = x->instance<Derived>();
对于使用虚函数重载而不维护自己的类型枚举的解析器AST的RTTI的常见替代方法是使用访问者模式,但根据我的经验,很快就会成为PITA。您仍然需要维护访客类别,但这可以进行细分和扩展。为了避免RTTI,你最终会得到很多样板代码。
另一种选择是为您感兴趣的语法类型创建虚函数。例如isNumeric()在Token基类中返回false但仅在Numeric类中重写以返回true。如果为虚拟函数提供默认实现,并且只在需要时才覆盖子类,那么大部分问题都将消失。
RTTI并不像以前那样糟糕。检查您正在阅读的文章的日期。人们也可以争辩说指针是一个非常糟糕的主意,但最后你会得到像Java这样的语言。