Sas*_*asQ 10 c++ parsing types class-design tokenize
我正在尝试编写一个可重用的解析库(为了好玩).
我写了一个Lexer生成序列的类Tokens.Token是子类层次结构的基类,每个子类表示不同的标记类型,具有自己的特定属性.例如,有一个子类LiteralNumber(从导出Literal,并通过它从Token),其具有用于处理其词位的数值其自身特定的方法.一般处理词位的方法(检索它们的字符串表示,源中的位置等)在基类中Token,因为它们对所有标记类型都是通用的.此类层次结构的用户可以为我未预测的特定令牌类型派生自己的类.
现在我有一个Parser类来读取标记流并尝试将它们与其语法定义相匹配.例如,它有一个方法matchExpression,该方法又调用matchTerm并调用此方法,该方法matchFactor必须测试当前标记是否为(Literal或者Name都是从Token基类派生的).
问题是:
我现在需要检查流中当前令牌的类型是什么,以及它是否与语法匹配.如果没有,抛出EParseError异常.如果是,则相应地采取行动以在表达式中获取其值,生成机器代码,或者在语法匹配时执行解析器需要执行的任何操作.
但是我已经阅读了很多关于在运行时检查类型并且从中做出决定,这是一个糟糕的设计™,它应该被重构为多态虚拟方法.当然,我同意这一点.
所以我的第一次尝试是type在Token基类中放置一些虚方法,这将被派生类重写并返回一些enum类型为id的类.
但是我已经看到了这种方法的缺点:从Token他们自己的令牌类派生的用户将无法添加额外的id enum,这是在库源中!: - /目标是允许他们在需要时扩展新类型令牌的层次结构.
我也可以string从type方法中返回一些,这样可以轻松定义新类型.
但是,在这两种情况下,基本类型的信息都会丢失(只有叶子类型从type方法返回),当有人从它派生并且覆盖它以返回其他内容时,Parser类将无法检测Literal派生类型type比"Literal".
当然,这个Parser类也是用户扩展的(也就是说,编写自己的解析器,识别自己的标记和语法)并不知道Token将来会有什么类的后代.
许多有关设计的常见问题解答和书籍建议在此场景中采用需要按类型决定的代码中的行为,并将其放入派生类中的虚拟方法覆盖中.但我无法想象如何将这种行为放入Token后代,因为例如,生成机器代码或计算表达式不是他们的业务.此外,语法的某些部分需要匹配多个令牌,因此没有一个特定的令牌可以将该行为放入其中.这是特定语法规则的责任,它可以匹配多个令牌作为其终端符号.
任何想法如何改进这种设计?
RTTI 得到所有主要 C++ 编译器的良好支持。这至少包括 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>();
Run Code Online (Sandbox Code Playgroud)
对于使用虚函数重载的解析器 AST,RTTI 的常见替代方案是使用访问者模式,但根据我的经验,这种模式很快就会变成 PITA,而无需维护自己的类型枚举。您仍然需要维护访问者类,但这可以进行子类化和扩展。为了避免 RTTI,您最终会得到大量样板代码。
另一种选择是为您感兴趣的语法类型创建虚函数。例如 isNumeric() 在 Token 基类中返回 false,但仅在 Numeric 类中被覆盖以返回 true。如果您为虚拟函数提供默认实现,并让子类仅在需要时进行重写,那么您的许多问题都会消失。
RTTI 不再像以前那么糟糕了。检查您正在阅读的文章的日期。也有人可能会说指针是一个非常糟糕的主意,但最终你会得到像 Java 这样的语言。
| 归档时间: |
|
| 查看次数: |
1034 次 |
| 最近记录: |