Смекни!
smekni.com

Эффективное использование STL и шаблонов (стр. 2 из 3)

SFiniteStateMachine( const StateType & StartState, const SFiniteStateMachineProxy & ProxyMachine );

Теперь очередь за реализацией описанных выше идей. Запишем конструктор автомата для рассматриваемого примера полностью.

SFiniteStateMachine FirstMachine( “empty”,(SFiniteStateMachineProxy() << (SStatesListProxy() << “empty” << “number” << “identifier” << “unknown”) <<(STransitionsProxy(letter) << “identifier” << “unknown” << “identifier” << “unknown”) <<(STransitionsProxy(digit) << “number” << “number” << “identifier” << “unknown”)));

На конструктор SFiniteStateMachine будет возложена задача проверки начального состояния. Оно должно быть в списке состояний.

Путем форматирования текста уже удалось придать аргументам конструктора вид таблицы. Однако это еще не все. При описании автомата были опущены все детали, связанные с шаблонами. На практике это означает, что при конструировании также придется указывать типы, что дополнительно “замусорит” текст. Несмотря на проблемы, связанные с препроцессором, он здесь поможет. Запись аргументов конструктора станет примерно такой:

FSMBEGIN( “empty” )FSMSTATES “empty” << “number” << “identifier” << “unknown”FSMEVENT(letter) “identifier” << “unknown” << “identifier” << “unknown”FSMEVENT(digit) “number” << “number” << “identifier” << “unknown”FSMEND

Такая запись уже приемлема для повседневного использования.

Детали реализации

Реализация должна включать ряд вспомогательных элементов, в частности, исключения. Автомат будет выдавать их в случае ошибки в описании состояний и переходов. При разработке своего класса исключений можно воспользоваться наследованием от класса стандартного исключения. Это даст возможность указать в блоке catch только ссылку на базовый стандартный класс исключений. Свой класс исключений можно определить так:

class SStateMachineException : public std::exception{ private: const std::string Message; public: SStateMachineException( const std::string & Msg ) : Message( Msg ) {} SStateMachineException( const char * Msg ) : Message( Msg ) {} virtual ~SStateMachineException() throw() {} virtual const char * what( void ) const throw() { return Message.c_str(); }private: SStateMachineException();};

В основной программе, использующей класс автомата, можно будет написать так:

. . .try{ . . . создание и использование автомата . . .}catch ( std::exception & Exception ){ // Поймаем и стандартное исключение и исключение, сгенерированное автоматом}

Вернемся к конструкторам. Поскольку они имеют дело со списками переменной длины, то для сохранения элементов логично воспользоваться контейнерами, предоставляемыми библиотекой STL ([3]). Для хранения одномерного списка воспользуемся контейнером vector, а для таблицы переходов – вектором векторов:

std::vector< std::vector<StateType> > Transitions;

Алгоритмы STL помогут находить событие в списке событий:

std::vector<EventType>::const_iterator k( std::find( Events.begin(), Events.end(), EntryEvent ) );

Поскольку контейнер vector поддерживает operator [], то для поиска состояния, в которое необходимо совершить переход, в таблице переходов можно воспользоваться подобной конструкцией:

NewState = Transitions[ EventIndex ] [ StateIndex ];

где соответствующие индексы могут быть вычислены с помощью алгоритма STL distance:

inline int GetStateIndex( const StateType & State ) const{ return std::distance( States.begin(), std::find( States.begin(), States.end(), State ) );}

Разумеется, класс автомата должен будет иметь функцию, принимающую и обрабатывающую событие. Существует два варианта. Первый – это функция, второй – перегрузка какого-либо оператора. Для придания дополнительной гибкости реализуем оба варианта:

SFiniteStateMachine & AcceptEvent( const EventType & Event ){ . . .}

и

inline SFiniteStateMachine & operator << ( const EventType & Event ){ return AcceptEvent( Event ); }

Перегрузка operator << даст возможность использовать автомат в таком стиле:

// Принять события Event1, Event2 и Event3MyMachine << Event1 << Event2 << Event3;

Остается вопрос: что делать, если придет событие, для которого у автомата нет описания переходов? Возможны варианты: просто проигнорировать такое событие, сгенерировать исключение или сделать что-то, определяемое пользователем. Воспользуемся идеей стратегий ([4]) и включим в число аргументов шаблона функтор, который будет определять нужную стратегию поведения. Такой подход вполне соответствует требованию 5. При этом можно задать стратегию по умолчанию – например, генерировать исключение. Теперь заголовок шаблона выглядит так:

template <class SState, class SEvent, class SUnknownEventStrategy = SThrowStrategy<SEvent> >class SFiniteStateMachine { . . . };

В числе заготовленных стратегий есть и стратегия игнорирования неизвестного события:

template <class SEvent>class SIgnoreStrategy{ public: inline void operator() ( const SEvent & ) const { return; }};

Если понадобятся другие действия, всегда можно написать собственный функтор по образу и подобию SIgnoreStrategy и передать его шаблону.

Многие источники, описывающие конечные автоматы, упоминают о возможности вызова функций при входе и выходе из состояния. Такую возможность легко предоставить, используя тот же подход стратегий. Функции входа и выхода из состояний удобно определять для класса, представляющего конкретное состояние. Вспоминая о требовании 5, дадим возможность гибкого управления такой возможностью. Предполагая, что функции класса состояния будут называться OnEnter и OnExit, можно написать несколько готовых функторов: не вызывающий ни одну из функций, вызывающий только OnEnter, вызывающий только OnExit и вызывающий обе функции.

template <class SState, class SEvent>class SEmptyFunctor{ public: inline void operator() ( SState & From, const SEvent & Event, SState & To ) { return; }};template <class SState, class SEvent>class SOnEnterFunctor{ public: inline void operator() ( SState & From, const SEvent & Event, SState & To ) { To.OnEnter( From, Event ); }};template <class SState, class SEvent>class SOnExitFunctor{ public: inline void operator() ( SState & From, const SEvent & Event, SState & To ) { From.OnExit( Event, To ); }};template <class SState, class SEvent>class SOnMoveFunctor{ public: inline void operator() ( SState & From, const SEvent & Event, SState & To ) { From.OnExit( Event, To ); To.OnEnter( From, Event ); }};

Стратегию по умолчанию (не вызывать никакую функцию) можно передать в качестве аргумента шаблона. Стратегия вызова функций, скорее всего, будет меняться чаще, чем стратегия действий при неизвестном событии. Поэтому ее имеет смысл поместить в списке аргументов перед стратегией реакции на неизвестное событие:

template <class SState, class SEvent, class SFunctor = SEmptyFunctor<SState,SEvent>, class SUnknownEventStrategy = SThrowStrategy<SEvent> >class SFiniteStateMachine { . . . };

Еще один вопрос, связанный с событиями, состоит в том, что событие может быть сгенерировано внутри функции, вызываемой при выходе или входе в состояние. Для обработки таких событий надо соответствующим образом спроектировать функцию, принимающую событие. С учетом таких “внутренних” событий, надо предусмотреть очередь, в которую будут помещаться события. Код, который обрабатывает переходы, должен будет делать это до тех пор, пока очередь не опустеет. В качестве контейнера, подходящего для хранения событий, воспользуемся deque из STL. Поскольку нам нужны только вставка элементов в начало и исключение из конца контейнера, без произвольного доступа, контейнер deque подойдет лучше всего.

Осталось совсем немного. Иногда нужно привести автомат в исходное состояние. Как и в случае с событиями предусмотрим два варианта: обычная функция и перегруженный operator <<. Для перегруженного operator << нужно определить специальный манипулятор:

enum SMachineManipulator { ResetMachine = 0};. . .inline SFiniteStateMachine & operator << ( SMachineManipulator Manipulator ) { if ( Manipulator == ResetMachine ) return Reset(); return *this; }

Теперь можно будет написать так:

// Принять событие Event1 и сбросить автомат в начальное состояниеMyMachine << Event1 << RestMachine;

Результатом работы автомата является состояние, в которое он перешел. Для получения текущего состояния напишем функцию и перегрузим оператор вывода в поток класса автомата:

inline StateType GetCurrentState( void ) const { return CurrentState; }template <class SState, class SEvent, class SFunctor, class SUnknownEventStrategy >ostream &operator<< (ostream & Stream, const SFiniteStateMachine<_SState, _SEvent, _SFunctor,_SUnknownEventStrategy> & Machine ){ return Stream << Machine.GetCurrentState(); }

Теперь, если для класса состояния определен оператор вывода в поток, можно написать такой фрагмент кода:

MyMachine << Event1 << RestMachine;cout << MyMachine; // Эквивалентно cout << MyMachine.GetCurrentState();

Как уже говорилось, для сокращения времени набора кода и удобочитаемости определены несколько макросов. Они требуют предварительного определения подстановки для типов событий и состояний. Требование связано с тем, что использование вложенных директив препроцессора невозможно. Шаблон же использует Proxy классы, которым также нужны сведения о типах. Поэтому для использования макросов придется сделать так:

#define FSMStateType string // Типсостояния#define FSMEventType int // Типсобытия. . .#undef FSMStateType#undef FSMEventType

Альтернатива есть: полностью указывать все типы.

Осталось поместить шаблон в пространство имен. После этого им можно пользоваться.

Пример использования шаблона

Напишем код для решения поставленной в начале статьи задачи.

#include <iostream>#include <string>using namespace std;#include "FiniteStateMachine.h"using namespace FSM;// Определимтипдлясобытийenum Events { letter = 0, digit = 1 };int main( int argc, char ** argv ){ #define FSMStateType string #define FSMEventType Events SFiniteStateMachine< StateType, EventType, SEmptyFunctor<StateType,EventType>, SThrowStrategy<EventType> > MyMachine( FSMBEGIN( "empty" ) FSMSTATES "empty" << "number" << "identifier" << "unknown" FSMEVENT(letter) "identifier" << "unknown" << "identifier" << "unknown" FSMEVENT(digit) "number" << "number" << "identifier" << "unknown" FSMEND ); #undef FSMStateType #undef FSMEventType cout << "StartState is: " << MyMachine << endl; MyMachine << digit << digit << letter; cout << "The 'unknown' state is expected. Current state is: " << MyMachine << endl;// Внимание: круглые скобки в следующей строке обязательны. Они обеспечат // правильный порядок выполнения операторовcout << "Reset the machine. Current state is: " << (MyMachine << ResetMachine) << endl; MyMachine << letter << digit << letter; cout << "The 'identifier' state is expected. Current state is: " << MyMachine << endl;return 0;}

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