Я столкнулся с проблемой дизайна некоторое время:
Я анализирую строку исходного кода в одномерный массив объектов токенов.
В зависимости от типа токена (буквенный, символ, идентификатор), он имеет некоторые специфические для типа токена данные. Литералы имеют значение, символы имеют тип символа, а идентификаторы имеют имя.
Затем я строю абстрактное представление сценария, определенного в этой строке исходного кода, на основе анализа этого 1-мерного массива токенов. Логика анализа синтаксиса выполняется вне этих токенов.
Моя проблема в том, что мне нужно, чтобы все мои токены, независимо от их типа, были сохранены в одном массиве, потому что это кажется более простым для анализа и потому что я не вижу другого способа сделать это. Это предполагает наличие общего типа для всех различных типов токенов, либо путем создания иерархии классов:
class token { token_type type; };
class identifier : public token { string name; };
class litteral : public token { value val; };
class symbol : public token( symbol_type sym; };
… или путем создания варианта:
class token
{
token_type type;
string name; // Only used when it is an identifier
value val; // Only used when it is a litteral
symbol_type sym; // Only used when it is a symbol
};
Иерархия классов будет использоваться следующим образом:
// Iterator over token array
for( auto cur_tok : tokens )
{
// Do token-type-specific things depending on its type
if( cur_token->type == E_SYMBOL )
{
switch( ((symbol *) cur_token)->symbol_type )
{
// etc
}
}
}
Но у него есть несколько проблем:
Базовый класс токенов должен знать о своих подклассах, что кажется неправильным.
Он включает в себя преобразование для доступа к конкретным данным в зависимости от типа токена, что, как мне сказали, также неверно.
Вариант решения будет использоваться аналогичным образом, без понижения:
for( auto cur_token: tokens )
{
if( cur_token->type == E_SYMBOL )
{
switch( cur_token->symbol_type )
{
// etc
}
}
}
Проблема этого второго решения состоит в том, что он смешивает все в один класс, что мне кажется не очень чистым, поскольку существуют неиспользуемые переменные в зависимости от типа токена и потому что класс должен представлять одну «вещь» тип.
У вас была бы другая возможность предложить дизайн? Мне рассказали о шаблоне посетителя, но я не представляю, как бы я использовал его в моем случае.
Я хотел бы сохранить возможность итерации по массиву, потому что мне, возможно, придется повторять в обоих направлениях, от случайной позиции и, возможно, несколько раз.
Спасибо.
Вариант 1: «толстый» тип с некоторыми общими / некоторыми выделенными полями
Выберите набор элементов данных, которые могут быть переназначены для конкретного типа токена для вашего «некоторые специфичные для типа токена данные. Литералы имеют значение, символы имеют тип символа, а идентификаторы имеют имя».
struct Token
{
enum Type { Literal, Symbol, Identifier } type_;
// fields repurposed per Token-Type
std::string s_; // text of literal or symbol or identifier
// fields specific to one Token-Type
enum Symbol_Id { A, B, C } symbol_id_;
};
Проблема заключается в том, что имена общих полей могут быть слишком расплывчатыми, чтобы они не вводили в заблуждение ни для какого конкретного типа токена, в то время как «определенные» поля по-прежнему доступны и могут злоупотреблять, когда токен другого типа.
Вариант 2: дискриминационный союз — желательно красиво упакованный для вас аля boost::variant<>
:
struct Symbol { ... };
struct Identifier { ... };
struct Literal { ... };
typedef boost::variant<Symbol, Identifier, Literal> Token;
std::list<Token> tokens;
Увидеть руководство для вариантов поиска данных.
Вариант 3: OOD — Классический объектно-ориентированный подход:
Почти то, что вы имели, но главное Token
Типу нужен виртуальный деструктор.
struct Token { virtual ~Token(); };
struct Identifier : Token { string name; };
struct Literal : Token { value val; };
struct Symbol : Token { symbol_type sym; };
std::vector<std::unique_ptr<Token>> tokens_;
tokens_.emplace_back(new Identifier { the_name });
Вам не нужно поле типа, так как вы можете использовать RTTI C ++, чтобы проверить, является ли конкретный Token*
адрес конкретного производного типа:
if (Literal* p = dynamic_cast<Literal>(tokens_[0].get()))
...it's a literal, can use p->val; ...
Вы были проблемы:
• Базовый класс токенов должен знать о своих подклассах, что кажется неправильным.
Не обязательно, учитывая RTTI.
• Он включает в себя приведение к определенным данным в зависимости от типа токена, что, как мне сказали, также неверно.
Часто, как это часто бывает в ОО, практично и желательно создать API базового класса, выражающий набор логических операций, которые может реализовать вся иерархия, но в вашем случае может возникнуть необходимость в «толстом» интерфейсе (что означает — множество операций, которые — если бы они были в API — могли бы сбить с толку no-ops (т.е. ничего не делать) или каким-либо образом (например, возвращаемое значение, исключения) сообщить, что многие операции не были поддержаны. Например, получение классификации типа символа isn ‘ не имеет смысла для символов, делающих его доступным только после dynamic_cast
это немного лучше, чем иметь его всегда доступным, но только иногда значимым, как в «варианте 1», потому что после приведения есть проверка использования во время компиляции.