Иерархия классов токенов и проверка их типа в синтаксическом анализаторе

Я пытаюсь написать многократно используемую библиотеку разбора (для удовольствия).

Я написал класс Lexer, который генерирует последовательность Tokens. Token - это базовый класс для иерархии подклассов, каждый из которых представляет собой различный тип токена, с его собственными специфическими свойствами. Например, существует подкласс LiteralNumber (полученный из Literal и через него из Token), который имеет свои собственные специальные методы для обработки числового значения его лексемы. Методы обработки лексем вообще (получение их символьного представления строки, позиция в источнике и т.д.) Находятся в базовом классе Token, потому что они являются общими для всех типов токенов. Пользователи этой иерархии классов могут выводить свои собственные классы для определенных типов токенов, которые не были предсказаны мной.

Теперь у меня есть класс Parser, который читает поток токенов и пытается сопоставить их с определением синтаксиса. Например, у него есть метод matchExpression, который, в свою очередь, вызывает matchTerm, и этот вызов вызывает matchFactor, который должен проверить, является ли текущий токен Literal или Name (оба получены из Token base класс).

Проблема заключается в следующем:
Мне нужно проверить, каков тип текущего токена в потоке и соответствует ли он синтаксису или нет. Если нет, бросьте исключение EParseError. Если да, действуйте соответственно, чтобы получить его значение в выражении, сгенерировать машинный код или сделать то, что должен выполнять парсер, когда синтаксис соответствует.

Но я много читал о том, что проверка типа во время выполнения и принятие решения из него - это bad design & trade;, и он должен быть реорганизован как полиморфные виртуальные методы. Конечно, я согласен с этим.

Итак, моя первая попытка состояла в том, чтобы поместить некоторый виртуальный метод type в базовый класс Token, который будет переопределяться производными классами и вернуть некоторый enum с идентификатором типа.

Но я уже вижу недостатки такого подхода: пользователи, получающие из Token собственные классы токенов, не смогут добавить дополнительный id к enum, который находится в источнике библиотеки!: -/И цель заключалась в том, чтобы позволить им расширять иерархию для новых типов токенов, когда они понадобятся.

Я также мог бы вернуть некоторый string из метода type, который позволит легко определять новые типы.

Но все же в обоих случаях информация о базовых типах теряется (только тип листа возвращается из метода type), а класс Parser не сможет определить производный тип Literal, когда кто-то извлек бы из него и переопределит type, чтобы вернуть что-то другое, кроме "Literal".

И, конечно же, класс Parser, который также предназначен для расширения пользователями (т.е. написание собственных парсеров, распознающих их собственные токены и синтаксис), не знает, какие потомки класса Token будут там в будущем.

Многие часто задаваемые вопросы и книги по дизайну рекомендуют в этом сценарии взять поведение из кода, который необходимо решить по типу, и поместить его в виртуальный метод, переопределяющий производные классы. Но я не могу представить, как я мог бы поместить это поведение в потомки Token, потому что это не их бизнес, например, для генерации машинного кода или оценки выражений. Кроме того, есть части синтаксиса, которые должны соответствовать более чем одному токену, поэтому нет ни одного конкретного токена, в который я мог бы поместить это поведение. Это скорее ответственность определенных правил синтаксиса, которые могли бы соответствовать более чем одному токену в качестве символов терминалов.

Любые идеи по улучшению этого дизайна?

Ответ 1

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>();

Общей альтернативой RTTI для анализатора AST, использующей перегрузку виртуальных функций, без поддержки собственного перечисления типов, является использование шаблона посетителя, но по моему опыту это быстро становится PITA. Вы все равно должны поддерживать класс посетителей, но это может быть подклассифицировано и расширено. В итоге вы получите много кода шаблона, чтобы избежать RTTI.

Другой вариант - просто создать виртуальные функции для интересующих вас синтаксических типов. Например, isNumeric(), который возвращает false в базовом классе Token, но переопределяется ТОЛЬКО в числовых классах для возврата true. Если вы обеспечиваете реализацию по умолчанию для своих виртуальных функций и позволяете подклассам переопределять только тогда, когда им нужно, тогда многие ваши проблемы исчезнут.

RTTI не так плохо TM, как когда-то. Проверьте даты в статьях, которые вы читаете. Можно также утверждать, что указатели - очень плохая идея, но тогда вы получаете такие языки, как Java.