【问题标题】:Class hierarchy of tokens and checking their type in the parser令牌的类层次结构并在解析器中检查它们的类型
【发布时间】:2011-09-09 13:13:19
【问题描述】:

我正在尝试编写一个可重用的解析库(为了好玩)。

我写了一个Lexer 类,它生成Tokens 的序列。 Token 是子类层次结构的基类,每个子类代表不同的令牌类型,具有自己的特定属性。例如,有一个子类LiteralNumber(从Literal 派生并从Token 继承),它有自己特定的方法来处理其词位的数值。一般处理词位的方法(检索它们的字符串表示、源中的位置等)在基类Token 中,因为它们对所有标记类型都是通用的。这个类层次结构的用户可以为我没有预料到的特定令牌类型派生自己的类。

现在我有一个Parser 类,它读取标记流并尝试将它们与其语法定义相匹配。例如,它有一个方法matchExpression,它又调用matchTerm,而这个又调用matchFactor,它必须测试当前令牌是Literal还是Name(两者都派生自Token base类)。

问题是:
我现在需要检查流中当前标记的类型以及它是否与语法匹配。如果不是,则抛出EParseError 异常。如果是,则采取相应措施以在表达式中获取其值,生成机器代码,或在语法匹配时执行解析器需要执行的任何操作。

但是我已经阅读了很多关于在运行时检查类型并从中做出决定的内容,这是一种糟糕的设计™,应该将其重构为多态虚拟方法。当然,我同意这一点。

所以我的第一次尝试是在Token 基类中放置一些type 虚方法,它会被派生类覆盖并返回一些带有类型id 的enum

但我已经看到了这种方法的一个缺点:从Token 派生出他们自己的令牌类的用户将无法向库源中的enum 添加额外的ID! :-/ 目标是允许他们在需要时扩展新类型令牌的层次结构。

我还可以从type 方法返回一些string,这样可以轻松定义新类型。

但是,在这两种情况下,关于基本类型的信息都会丢失(只有叶类型从 type 方法返回)并且Parser 类将无法检测到Literal 派生类型有人会从中派生并覆盖 type 以返回除 "Literal" 以外的其他内容。

当然,Parser 类也用于扩展用户(即编写自己的解析器,识别自己的标记和语法)不知道 Token 类的后代将是什么将来会有。

许多常见问题解答和设计书籍都建议在这种情况下从需要按类型决定的代码中获取行为,并将其放入派生类中重写的虚拟方法中。但我无法想象我怎么能把这种行为放到Token 后代中,因为这不是他们的业务,例如,生成机器代码或评估表达式。此外,语法的某些部分需要匹配多个标记,因此没有一个特定的标记可以让我将该行为放入其中。这是特定语法规则的责任,它可以匹配多个标记作为它们的终端符号。

有什么想法可以改进这个设计吗?

【问题讨论】:

  • +1,我每次写解析器时都会问自己同样的问题(我已经写过好几个了)。
  • 我会引用谷歌风格指南:“不要手动实现类似 RTTI 的解决方法。反对 RTTI 的论点同样适用于带有类型标签的类层次结构等解决方法。”我个人不同意运行时类型检查总是是坏事。
  • 我知道选择 RTTI 代替手工类型标签的理由。这正是我在上面的问题中描述的问题(尽管可能不够冗长)。我正在寻找一种方法来用语言中已经存在的更好、更灵活的方法来替换这种类型标记方法。但我也听说了使用这些内置 RTTI 机制的差异(不可移植性、性能损失等),所以我很好奇它是否更好。
  • 不要重新发明方轮,例如检查 boost::spirit。
  • @Gene:除了学习或者当你可以让它更好更适合你自己的使用时(例如,我已经跳过了精神,因为对于具有基本诊断的简单的类似 LISP 的语法的编译时间很长输出;LLVM 是最合适的,但我也跳过了它,因为它为我的项目注入了巨大的依赖关系)

标签: c++ parsing types class-design tokenize


【解决方案1】:

所有主要的 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 这样的语言。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-08-25
    • 1970-01-01
    • 2022-10-18
    • 2013-12-25
    • 2013-03-18
    • 1970-01-01
    • 2012-01-02
    • 1970-01-01
    相关资源
    最近更新 更多