C ++ Избегание понижения или вариантов

Я столкнулся с проблемой дизайна некоторое время:

  • Я анализирую строку исходного кода в одномерный массив объектов токенов.

  • В зависимости от типа токена (буквенный, символ, идентификатор), он имеет некоторые специфические для типа токена данные. Литералы имеют значение, символы имеют тип символа, а идентификаторы имеют имя.

  • Затем я строю абстрактное представление сценария, определенного в этой строке исходного кода, на основе анализа этого 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
}
}
}

Проблема этого второго решения состоит в том, что он смешивает все в один класс, что мне кажется не очень чистым, поскольку существуют неиспользуемые переменные в зависимости от типа токена и потому что класс должен представлять одну «вещь» тип.

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

Я хотел бы сохранить возможность итерации по массиву, потому что мне, возможно, придется повторять в обоих направлениях, от случайной позиции и, возможно, несколько раз.

Спасибо.

2

Решение

Вариант 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», потому что после приведения есть проверка использования во время компиляции.

3

Другие решения


По вопросам рекламы [email protected]