Библиотека Boost Statechart - это фреймворк, который позволяет быстро преобразовывать UML Statechart в исполняемый код C++без необходимости использования генератора кода. Благодаря поддержке почти всех функций UML преобразование происходит прямолинейно, а полученный код C++ представляет собой текстовое описание Statechart без избыточности.
Этот учебник был разработан для линейного чтения. В первый раз пользователи должны начать читать прямо в начале и остановиться, как только они узнают достаточно для выполнения задачи. В частности:
Небольшие и простые машины с несколькими состояниями могут быть реализованы достаточно хорошо, используя функции, описанные вОсновные темы: Стоп-часы
Наконец, пользователи хотят создавать еще более сложные машины и проектные архитекторы, оценивающие Boost. В конце Statechart также следует прочитать разделРасширенные темы. Более того, настоятельно рекомендуется прочитать разделОграниченияв Обосновании.
Мы будем использовать самую простую программу, чтобы сделать первые шаги. Государственная карта...
... реализуется со следующим кодом:
#include <boost/statechart/state_machine.hpp>
#include <boost/statechart/simple_state.hpp>
#include <iostream>
namespace sc = boost::statechart;
// We are declaring all types as structs only to avoid having to
// type public. If you don't mind doing so, you can just as well
// use class.
// We need to forward-declare the initial state because it can
// only be defined at a point where the state machine is
// defined.
struct Greeting;
// Boost.Statechart makes heavy use of the curiously recurring
// template pattern. The deriving class must always be passed as
// the first parameter to all base class templates.
//
// The state machine must be informed which state it has to
// enter when the machine is initiated. That's why Greeting is
// passed as the second template parameter.
struct Machine : sc::state_machine< Machine, Greeting > {};
// For each state we need to define which state machine it
// belongs to and where it is located in the statechart. Both is
// specified with Context argument that is passed to
// simple_state<>. For a flat state machine as we have it here,
// the context is always the state machine. Consequently,
// Machine must be passed as the second template parameter to
// Greeting's base (the Context parameter is explained in more
// detail in the next example).
struct Greeting : sc::simple_state< Greeting, Machine >
{
// Whenever the state machine enters a state, it creates an
// object of the corresponding state class. The object is then
// kept alive as long as the machine remains in the state.
// Finally, the object is destroyed when the state machine
// exits the state. Therefore, a state entry action can be
// defined by adding a constructor and a state exit action can
// be defined by adding a destructor.
Greeting() { std::cout << "Hello World!\n"; } // entry
~Greeting() { std::cout << "Bye Bye World!\n"; } // exit
};
int main()
{
Machine myMachine;
// The machine is not yet running after construction. We start
// it by calling initiate(). This triggers the construction of
// the initial state Greeting
myMachine.initiate();
// When we leave main(), myMachine is destructed what leads to
// the destruction of all currently active states.
return 0;
}
Две кнопки смоделированы двумя событиями. Кроме того, мы также определяем необходимые состояния и исходное состояние.Следующий код является нашей отправной точкой, последующие фрагменты кода должны быть вставлены:
#include <boost/statechart/event.hpp>
#include <boost/statechart/state_machine.hpp>
#include <boost/statechart/simple_state.hpp>
namespace sc = boost::statechart;
struct EvStartStop : sc::event< EvStartStop > {};
struct EvReset : sc::event< EvReset > {};
struct Active;
struct StopWatch : sc::state_machine< StopWatch, Active > {};
struct Stopped;
// The simple_state class template accepts up to four parameters:
// - The third parameter specifies the inner initial state, if
// there is one. Here, only Active has inner states, which is
// why it needs to pass its inner initial state Stopped to its
// base
// - The fourth parameter specifies whether and what kind of
// history is kept
// Active is the outermost state and therefore needs to pass the
// state machine class it belongs to
struct Active : sc::simple_state<
Active, StopWatch, Stopped > {};
// Stopped and Running both specify Active as their Context,
// which makes them nested inside Active
struct Running : sc::simple_state< Running, Active > {};
struct Stopped : sc::simple_state< Stopped, Active > {};
// Because the context of a state must be a complete type (i.e.
// not forward declared), a machine must be defined from
// "outside to inside". That is, we always start with the state
// machine, followed by outermost states, followed by the direct
// inner states of outermost states and so on. We can do so in a
// breadth-first or depth-first way or employ a mixture of the
// two.
int main()
{
StopWatch myWatch;
myWatch.initiate();
return 0;
}
Это компилируется, но пока не делает ничего наблюдаемого.
На данный момент мы будем использовать только один тип реакции: переходы. Мывставляемжирные части следующего кода:
#include <boost/statechart/transition.hpp>
// ...
struct Stopped;
struct Active : sc::simple_state< Active, StopWatch, Stopped >
{
typedef sc::transition< EvReset, Active > reactions;
};
struct Running : sc::simple_state< Running, Active >
{
typedef sc::transition< EvStartStop, Stopped > reactions;
};
struct Stopped : sc::simple_state< Stopped, Active >
{
typedef sc::transition< EvStartStop, Running > reactions;
};
// A state can define an arbitrary number of reactions. That's
// why we have to put them into an mpl::list<> as soon as there
// is more than one of them
// (see Specifying multiple reactions for a state).
int main()
{
StopWatch myWatch;
myWatch.initiate();
myWatch.process_event( EvStartStop() );
myWatch.process_event( EvStartStop() );
myWatch.process_event( EvStartStop() );
myWatch.process_event( EvReset() );
return 0;
}
Теперь у нас есть все штаты и все переходы, и ряд событий также отправляется на стопор. Машина покорно делает переходы, которые мы ожидали бы, но никаких действий пока не выполняется.
Затем мы сделаем так, чтобы секундомеры действительно измеряли время. В зависимости от состояния стоп-часа, нам нужны разные переменные:
Остановка: Одна переменная, удерживающая прошедшее время
Бег: Одна переменная, удерживающая прошедшее времяиодна переменная, сохраняющая точку времени, в которой часы в последний раз были запущены.
Мы видим, что переменная времени необходима независимо от того, в каком состоянии находится машина. Более того, эта переменная должна быть сброшена до 0, когда мы отправляем в машину событие<EvReset>. Другая переменная необходима только в то время, когда машина находится в запущенном состоянии. Он должен быть установлен на текущее время системных часов всякий раз, когда мы входим в состояние Running. При выходе мы просто вычитаем время начала из текущего системного времени и добавляем результат к прошедшему времени.
#include <ctime>
// ...
struct Stopped;
struct Active : sc::simple_state< Active, StopWatch, Stopped >
{
public:
typedef sc::transition< EvReset, Active > reactions;
Active() : elapsedTime_( 0.0 ) {}
double ElapsedTime() const { return elapsedTime_; }
double & ElapsedTime() { return elapsedTime_; }
private:
double elapsedTime_;
};
struct Running : sc::simple_state< Running, Active >
{
public:
typedef sc::transition< EvStartStop, Stopped > reactions;
Running() : startTime_( std::time( 0 ) ) {}
~Running()
{
// Similar to when a derived class object accesses its
// base class portion, context<>() is used to gain
// access to the direct or indirect context of a state.
// This can either be a direct or indirect outer state
// or the state machine itself
// (e.g. here: context< StopWatch >()).
context< Active >().ElapsedTime() +=
std::difftime( std::time( 0 ), startTime_ );
}
private:
std::time_t startTime_;
};
// ...
Машина теперь измеряет время, но мы пока не можем извлечь его из основной программы.
На данный момент преимущества локального государственного хранилища (которое до сих пор является относительно малоизвестной особенностью) еще не стали очевидными. Пункт FAQ"Что такого крутого в местном хранилище?"пытается объяснить их более подробно, сравнивая этот StopWatch с тем, который не использует государственно-местное хранилище.
Чтобы получить измеренное время, нам нужен механизм для получения информации о состоянии из машины. С нашим нынешним дизайном машины есть два способа сделать это. Для простоты мы используем менее эффективный вариант:<state_cast<>()>(StopWatch2.cpp показывает немного более сложную альтернативу). Как следует из названия, семантика очень похожа на семантику<dynamic_cast>. Например, когда мы называем<myWatch.state_cast< const Stopped & >()>имашину в настоящее время в состоянии Stopped, мы получаем ссылку на состояние<Stopped>. В противном случае<std::bad_cast>. Мы можем использовать эту функциональность для реализации функции<StopWatch>, которая возвращает прошедшее время. Однако вместо того, чтобы спрашивать машину, в каком состоянии она находится, а затем переключаться на различные вычисления за прошедшее время, мы помещаем вычисление в состояния Stopped and Running и используем интерфейс для извлечения прошедшего времени:
Чтобы действительно увидеть время, измеряемое, вы можете пройти один шаг через утверждения<main()>. Пример StopWatch расширяет эту программу до интерактивного консольного приложения.
Пока так хорошо. Однако представленный выше подход имеет несколько ограничений:
Плохая масштабируемость: Как только компилятор достигает точки вызоваstate_machine::initiate(), происходит ряд шаблонных инстанциаций, которые могут быть успешными только в том случае, если известна полная декларация каждого состояния машины. То есть вся компоновка государственной машины должна быть реализована в одном едином блоке перевода (действия могут быть составлены отдельно, но здесь это не имеет значения). Для более крупных (и более реальных) государственных машин это приводит к следующим ограничениям:
В какой-то момент компиляторы достигают своих внутренних пределов и сдаются. Это может произойти даже для машин среднего размера. Например, в режиме отладки один популярный компилятор отказался компилировать более ранние версии примера BitMachine для чего-либо выше 3 бит. Это означает, что компилятор достиг своего предела где-то между 8 состояниями, 24 переходами и 16 состояниями, 64 переходами.
Несколько программистов вряд ли могут работать на одной и той же машине состояния одновременно, потому что каждое изменение макета неизбежно приведет к перекомпиляции всей машины состояния.
Максимальная реакция на событие: Согласно UML, состояние может иметь несколько реакций, вызванных одним и тем же событием. Это имеет смысл, когда все реакции имеют взаимоисключающие защитные механизмы. Интерфейс, который мы использовали выше, допускает не более одной неохраняемой реакции для каждого события. Кроме того, переход и точка выбора концепций UML не поддерживаются напрямую.
Все эти ограничения можно преодолеть с помощью пользовательских реакций.Предупреждение: Легко злоупотреблять пользовательскими реакциями вплоть до вызова неопределенного поведения. Пожалуйста, изучите документацию, прежде чем использовать ее!
Несколько других кнопок, которые здесь не интересны
Один из вариантов использования камеры говорит, что фотограф может наполовину нажать затворв любом местев режиме конфигурации, и камера немедленно перейдет в режим съемки. Следующая диаграмма состояния является одним из способов достижения этого поведения:
Конфигурирующие и стреляющие состояния будут содержать множество вложенных состояний, в то время как состояние холостого хода относительно просто. Поэтому было принято решение о создании двух команд. Один будет реализовывать режим съемки, а другой будет реализовывать режим конфигурации. Обе команды уже договорились об интерфейсе, который съемочная группа будет использовать для извлечения настроек конфигурации. Мы хотели бы обеспечить, чтобы две команды могли работать с наименьшим возможным вмешательством. Таким образом, мы помещаем два состояния в их собственные блоки перевода, так что изменения компоновки машины в состоянии Конфигурирования никогда не приведут к перекомпиляции внутренней работы состояния Стрельбы и наоборот.
В отличие от предыдущего примера, выдержки, представленные здесь, часто описывают различные варианты достижения того же эффекта. Вот почему код часто не равен коду примера камеры.Комментарии обозначают те части, где это имеет место.
Camera.hpp:
#ifndef CAMERA_HPP_INCLUDED
#define CAMERA_HPP_INCLUDED
#include <boost/statechart/event.hpp>
#include <boost/statechart/state_machine.hpp>
#include <boost/statechart/simple_state.hpp>
#include <boost/statechart/custom_reaction.hpp>
namespace sc = boost::statechart;
struct EvShutterHalf : sc::event< EvShutterHalf > {};
struct EvShutterFull : sc::event< EvShutterFull > {};
struct EvShutterRelease : sc::event< EvShutterRelease > {};
struct EvConfig : sc::event< EvConfig > {};
struct NotShooting;
struct Camera : sc::state_machine< Camera, NotShooting >
{
bool IsMemoryAvailable() const { return true; }
bool IsBatteryLow() const { return false; }
};
struct Idle;
struct NotShooting : sc::simple_state<
NotShooting, Camera, Idle >
{
// With a custom reaction we only specify that we might do
// something with a particular event, but the actual reaction
// is defined in the react member function, which can be
// implemented in the .cpp file.
typedef sc::custom_reaction< EvShutterHalf > reactions;
// ...
sc::result react( const EvShutterHalf & );
};
struct Idle : sc::simple_state< Idle, NotShooting >
{
typedef sc::custom_reaction< EvConfig > reactions;
// ...
sc::result react( const EvConfig & );
};
#endif
Camera.cpp:
#include "Camera.hpp"
// The following includes are only made here but not in
// Camera.hpp
// The Shooting and Configuring states can themselves apply the
// same pattern to hide their inner implementation, which
// ensures that the two teams working on the Camera state
// machine will never need to disturb each other.
#include "Configuring.hpp"
#include "Shooting.hpp"
// ...
// not part of the Camera example
sc::result NotShooting::react( const EvShutterHalf & )
{
return transit< Shooting >();
}
sc::result Idle::react( const EvConfig & )
{
return transit< Configuring >();
}
Осторожно: Любой призыв к<simple_state<>::transit<>()>или<simple_state<>::terminate()>(см.ссылка) неизбежно уничтожит государственный объект (аналогично<delete this;>)! То есть код, исполняемый после любого из этих вызовов, может вызывать неопределенное поведение!Вот почему эти функции следует называть только частью заявления о возврате.
Внутренняя работа состояния стрельбы может выглядеть следующим образом:
Когда пользователь наполовину нажимает затвор, вводится Съемка и его внутреннее начальное состояние Фокусировка. В действии ввода фокусировки камера инструктирует схему фокусировки, чтобы привести объект в фокус. Затем схема фокусировки соответствующим образом перемещает линзы и отправляет событие EvInFocus, как только оно будет сделано. Конечно, пользователь может полностью нажать затвор, пока линзы все еще находятся в движении. Без каких-либо мер предосторожности событие EvShutterFull будет просто потеряно, потому что состояние фокусировки не определяет реакцию на это событие. В результате пользователю придется снова полностью нажимать затвор после того, как камера закончит фокусировку. Чтобы предотвратить это, событие EvShutterFull откладывается в состоянии фокусировки. Это означает, что все события такого типа хранятся в отдельной очереди, которая опорожняется в основную очередь при выходе состояния фокусировки.
Оба перехода, происходящие в сфокусированном состоянии, вызваны одним и тем же событием, но у них есть взаимоисключающие защитные механизмы. Вот подходящая пользовательская реакция:
// not part of the Camera example
sc::result Focused::react( const EvShutterFull & )
{
if ( context< Camera >().IsMemoryAvailable() )
{
return transit< Storing >();
}
else
{
// The following is actually a mixture between an in-state
// reaction and a transition. See later on how to implement
// proper transition actions.
std::cout << "Cache memory full. Please wait...\n";
return transit< Focused >();
}
}
Конечно, пользовательские реакции могут быть реализованы непосредственно в государственной декларации, которая часто предпочтительнее для простого просмотра.
Далее мы будем использовать стражу, чтобы предотвратить переход и позволить внешним состояниям реагировать на событие, если батарея низкая:
Camera.cpp:
// ...
sc::result NotShooting::react( const EvShutterHalf & )
{
if ( context< Camera >().IsBatteryLow() )
{
// We cannot react to the event ourselves, so we forward it
// to our outer state (this is also the default if a state
// defines no reaction for a given event).
return forward_event();
}
else
{
return transit< Shooting >();
}
}
// ...
Самопереход сфокусированного состояния также может быть реализован как реакцияв состоянии, которая имеет тот же эффект, если сфокусированное не имеет каких-либо действий входа или выхода:
Shooting.cpp:
// ...
sc::result Focused::react( const EvShutterFull & )
{
if ( context< Camera >().IsMemoryAvailable() )
{
return transit< Storing >();
}
else
{
std::cout << "Cache memory full. Please wait...\n";
// Indicate that the event can be discarded. So, the
// dispatch algorithm will stop looking for a reaction
// and the machine remains in the Focused state.
return discard_event();
}
}
// ...
Поскольку реакция внутри государства защищена, мы должны использовать<custom_reaction<>>здесь. Для неохраняемых реакций в состоянии<in_state_reaction<>>следует использовать для лучшей читаемости кода.
Начиная с самого внутреннего общего контекста, все действия входа до целевого состояния с последующими действиями входа начальных состояний.
Пример:
Здесь порядок следующий: ~D(), ~C(), ~B(), ~A(), t(), X(), Y(), Z(). Таким образом, переходное действие t() выполняется в контексте состояния InnermostCommonOuter, поскольку исходное состояние уже оставлено (уничтожено) и целевое состояние еще не введено (построено).
С Бустом. Statechart, переходное действие может быть членомлюбогообщего внешнего контекста. То есть переход между фокусировкой и фокусировкой может осуществляться следующим образом:
Shooting.hpp:
// ...
struct Focusing;
struct Shooting : sc::simple_state< Shooting, Camera, Focusing >
{
typedef sc::transition<
EvShutterRelease, NotShooting > reactions;
// ...
void DisplayFocused( const EvInFocus & );
};
// ...
// not part of the Camera example
struct Focusing : sc::simple_state< Focusing, Shooting >
{
typedef sc::transition< EvInFocus, Focused,Shooting, &Shooting::DisplayFocused > reactions;
};
Иливозможно также следующее (здесь в качестве внешнего контекста выступает сама государственная машина):
// not part of the Camera example
struct Camera : sc::state_machine< Camera, NotShooting >
{
void DisplayFocused( const EvInFocus & );
};
// not part of the Camera example
struct Focusing : sc::simple_state< Focusing, Shooting >
{
typedef sc::transition< EvInFocus, Focused,Camera, &Camera::DisplayFocused > reactions;
};
Естественно, переходные действия также могут быть вызваны пользовательскими реакциями:
Shooting.cpp:
// ...
sc::result Focusing::react( const EvInFocus & evt )
{
// We have to manually forward evt
return transit< Focused >( &Shooting::DisplayFocused, evt );
}
Мероприятие выдвигается в главную очередь. События в очереди обрабатываются, как только завершается текущая реакция. События могут быть размещены внутри<react>функций, действий входа, выхода и перехода. Тем не менее, размещение действий внутри входа немного сложнее (см., например,<Focusing::Focusing()>в<Shooting.cpp>в примере камеры):
Как только поступательное действие государства должно вступить в контакт с «внешним миром» (здесь: очередь событий в машине состояния), государство должно исходить из<state<>>, а не из<simple_state<>>и должно реализовать пересылающий конструктор, как описано выше (кроме конструктора,<state<>>предлагает тот же интерфейс, что и<simple_state<>>). Следовательно, это должно быть сделано всякий раз, когда действие ввода делает один или несколько вызовов к следующим функциям:
simple_state<>::post_event()
simple_state<>::clear_shallow_history<>()
simple_state<>::clear_deep_history<>()
simple_state<>::outermost_context()
simple_state<>::context<>()
simple_state<>::state_cast<>()
simple_state<>::state_downcast<>()
simple_state<>::state_begin()
simple_state<>::state_end()
По моему опыту, эти функции необходимы только редко в действиях ввода, поэтому этот обходной путь не должен слишком сильно уродовать пользовательский код.
Фотографы, тестирующие бета-версии нашейцифровой камеры, сказали, что им очень понравилось, что полунажатие затвора в любое время (даже когда камера настраивается) немедленно готовит камеру к съемке. Однако большинство из них сочли нелогичным, что камера всегда переходит в режим холостого хода после выпуска затвора. Они предпочли бы, чтобы камера вернулась в то состояние, в котором она была до полунажатия затвора. Таким образом, они могут легко проверить влияние настройки конфигурации, изменив ее, наполовину, а затем полностью нажав затвор, чтобы сделать снимок. Наконец, выпуск затвора приведет их обратно на экран, где они изменили настройку. Чтобы реализовать это поведение, мы изменили график состояния следующим образом:
Как упоминалось ранее, конфигурирующее состояние содержит довольно сложную и глубоко вложенную внутреннюю машину. Естественно, мы хотели бы восстановить предыдущее состояние вплоть довнутреннего состояния(s) в Конфигурировании, поэтому мы используем псевдосостояние глубокой истории. Соответствующий код выглядит следующим образом:
// not part of the Camera example
struct NotShooting : sc::simple_state<
NotShooting, Camera, Idle, sc::has_deep_history >
{
// ...
};
// ...
struct Shooting : sc::simple_state< Shooting, Camera, Focusing >
{
typedef sc::transition<
EvShutterRelease, sc::deep_history< Idle > > reactions;
// ...
};
История имеет два этапа: Во-первых, при выходе из состояния, содержащего историю псевдогосударства, необходимо сохранить информацию о ранее активной иерархии внутреннего состояния. Во-вторых, при переходе к истории псевдосостояния информация о сохраненной государственной иерархии должна быть извлечена и введены соответствующие состояния. Первый выражается переходом либо<has_shallow_history>,<has_deep_history>, либо<has_full_history>(который сочетает в себе неглубокую и глубокую историю) в качестве последнего параметра к шаблонам классов<simple_state>и<state>. Последнее выражается указанием либо<shallow_history<>>, либо<deep_history<>>как места перехода, либо, как мы увидим в одно мгновение, как внутреннего начального состояния. Поскольку возможно, что состояние, содержащее псевдосостояние истории, никогда не было введено до перехода к истории, оба шаблона классов требуют параметра, определяющего состояние по умолчанию, для входа в такие ситуации.
Избыточность, необходимая для использования истории, проверяется на согласованность во время компиляции. То есть государственная машина не составила бы, если бы мы забыли пройти<has_deep_history>к базе<NotShooting>.
В другом запросе на изменение, поданном несколькими бета-тестерами, говорится, что они хотели бы, чтобы камера вернулась в то состояние, в котором она была до выключения. Вот реализация:
// ...
// not part of the Camera example
struct NotShooting : sc::simple_state< NotShooting, Camera,
mpl::list< sc::deep_history< Idle > >,
sc::has_deep_history >
{
// ...
};
// ...
К сожалению, есть небольшое неудобство из-за некоторых деталей реализации, связанных с шаблонами. Когда внутреннее начальное состояние представляет собой инстанциацию шаблона класса, мы всегда должны поместить его в<mpl::list<>>, хотя есть только одно внутреннее начальное состояние. Более того, нынешняя глубокая история реализации имеет некоторыеограничения.
<orthogonal< 0 >>является по умолчанию, поэтому<NumLockOn>и<NumLockOff>могут просто пройти<Active>вместо<Active::orthogonal< 0 >>, чтобы указать их контекст. Числа, переданные шаблону<orthogonal>, должны соответствовать позиции списка во внешнем состоянии. Кроме того, ортогональное положение исходного состояния перехода должно соответствовать ортогональному положению целевого состояния. Любые нарушения этих правил приводят к составлению временных ошибок. Примеры:
// Example 1: does not compile because Active specifies
// only 3 orthogonal regions
struct WhateverLockOn: sc::simple_state<
WhateverLockOn, Active::orthogonal< 3 > > {};
// Example 2: does not compile because Active specifies
// that NumLockOff is part of the "0th" orthogonal region
struct NumLockOff : sc::simple_state<
NumLockOff, Active::orthogonal< 1 > > {};
// Example 3: does not compile because a transition between
// different orthogonal regions is not permitted
struct CapsLockOn : sc::simple_state<
CapsLockOn, Active::orthogonal< 1 > >
{
typedef sc::transition<
EvCapsLockPressed, CapsLockOff > reactions;
};
struct CapsLockOff : sc::simple_state<
CapsLockOff, Active::orthogonal< 2 > >
{
typedef sc::transition<
EvCapsLockPressed, CapsLockOn > reactions;
};
Часто реакции в машине состояния зависят от активного состояния в одной или нескольких ортогональных областях. Это связано с тем, что ортогональные области не являются полностью ортогональными или определенная реакция во внешнем состоянии может иметь место только в том случае, если внутренние ортогональные области находятся в конкретных состояниях. Для этой цели функция<state_cast<>>, введенная в соответствии сПолучение информации о состоянии из машины, также доступна в государствах.
В качестве несколько надуманного примера предположим, что нашаклавиатуратакже принимает<EvRequestShutdown>события, прием которых заставляет клавиатуру прекращать работу только в том случае, если все ключи блокировки находятся в выключенном состоянии. Затем мы изменим устройство состояния клавиатуры следующим образом:
Прохождение типа указателя вместо эталонного типа приводит к возврату 0 указателей вместо того, чтобы<std::bad_cast>выбрасываться при отказе литья. Также обратите внимание на использование<state_downcast<>()>вместо<state_cast<>()>. Подобно различиям между<boost::polymorphic_downcast<>()>и<dynamic_cast>,<state_downcast<>()>является гораздо более быстрым вариантом<state_cast<>()>и может использоваться только тогда, когда пройденный тип является наиболее производным типом.<state_cast<>()>следует использовать только в том случае, если вы хотите запросить дополнительную базу.
Custom state queries
Часто желательно выяснить, в каком состоянии (состояниях) находится машина в настоящее время. В какой-то степени это уже возможно с<state_cast<>()>и<state_downcast<>()>, но их полезность довольно ограничена, потому что оба возвращают только ответ да/нет на вопрос «Вы в состоянии Х?». Можно задавать более сложные вопросы, когда вы проходите дополнительный базовый класс, а не государственный класс до<state_cast<>()>, но это требует больше работы (все государства должны извлекать и реализовывать дополнительную базу), медленно (под капотом<state_cast<>()>использует<dynamic_cast>), заставляет проекты компилироваться с включенным C++ RTTI и оказывает негативное влияние на скорость входа / выхода государства.
Особенно для отладки было бы гораздо полезнее, если бы можно было спросить: «В каком состоянии вы находитесь?» Для этого можно повторять все активныевнутренниесостояния с<state_machine<>::state_begin()>и<state_machine<>::state_end()>. Ссылка на возвращенный итератор возвращает ссылку на<const
state_machine<>::state_base_type>, общую основу всех состояний. Таким образом, мы можем распечатать конфигурацию текущего активного состояния следующим образом (см. пример клавиатуры для полного кода):
void DisplayStateConfiguration( const Keyboard & kbd )
{
char region = 'a';
for (
Keyboard::state_iterator pLeafState = kbd.state_begin();
pLeafState != kbd.state_end(); ++pLeafState )
{
std::cout << "Orthogonal region " << region << ": ";
// The following use of typeid assumes that
// BOOST_STATECHART_USE_NATIVE_RTTI is defined
std::cout << typeid( *pLeafState ).name() << "\n";
++region;
}
}
При необходимости к внешним состояниям можно получить доступ с помощью<state_machine<>::state_base_type::outer_state_ptr()>, который возвращает указатель на<const
state_machine<>::state_base_type>. При вызове внешнего состояния эта функция просто возвращается 0.
Чтобы уменьшить исполняемый размер, некоторые приложения должны быть скомпилированы с отключенным C++ RTTI. Это сделало бы возможность итерации по всем активным состояниям практически бесполезной, если бы не следующие две функции:
Оба возвращают значение, которое сопоставимо через<operator==()>и<std::less<>>. Одного этого было бы достаточно для реализации вышеупомянутой функции<DisplayStateConfiguration>без помощи<typeid>, но это все еще несколько громоздко, поскольку карта должна использоваться для связи значений информации типа с именами состояний.
Исключения могут распространяться из всего кода пользователя, кроме государственных деструкторов. Из коробки структура государственной машины сконфигурирована для простой обработки исключений и не улавливает ни одно из этих исключений, поэтому они немедленно распространяются на клиент государственной машины. Охрана прицела внутри<state_machine<>>гарантирует, что все государственные объекты будут уничтожены до того, как клиент поймает исключение. Охранник прицела не пытается вызвать какие-либо<exit>функции (см.). Две стадии выходаниже, которые государства могли бы определить как эти могли бы сами бросить другие исключения, которые маскировали бы первоначальное исключение. Следовательно, если государственная машина должна делать что-то более разумное, когда выбрасываются исключения, она должна поймать их, прежде чем они будут распространяться в Рост. Структура Statechart. Эта схема обработки исключений часто уместна, но она может привести к значительному дублированию кода в государственных машинах, где многие действия могут вызвать исключения, которые необходимо обрабатывать внутри государственной машины (см.Обработка ошибокв Rationale). Вот почему обработка исключений может быть настроена с помощью<ExceptionTranslator>параметра шаблона<state_machine>класса. Поскольку поведение вне коробкинепереводит какие-либо исключения, аргумент по умолчанию для этого параметра<null_exception_translator>. Подтип<state_machine<>>может быть сконфигурирован для расширенной обработки исключений путем указания вместо этого библиотеки<exception_translator<>>. Таким образом, следующее происходит, когда из кода пользователя распространяется исключение:
Исключение улавливается внутри каркаса
В блоке улова на стеке выделяется событиеexception_thrown.
Также в блоке улова предпринята попытканемедленногоотправкиexception_thrownсобытия. То есть, возможно, оставшиеся события в очереди отправляются только после того, как исключение было успешно обработано.
Если исключение было успешно обработано, государственная машина обычно возвращается к клиенту. Если исключение не может быть успешно обработано, первоначальное исключение переносится, чтобы клиент государственной машины мог справиться с исключением.
На платформах с багги-исключениями для обработки реализаций пользователи, вероятно, захотят реализовать свою собственную модель концепцииExceptionTranslator(см. такжеДискриминирующие исключения).
Successful exception handling
Исключение считается выполненным успешно, если:
Найдена соответствующая реакция на событиеexception_thrown,и.
Машина состояния находится в стабильном состоянии после завершения реакции.
Второе условие важно для сценариев 2 и 3 в следующем разделе. В этих сценариях государственная машина находится в середине перехода, когда обрабатывается исключение. Машина останется в недействительном состоянии, если реакция просто отбросит событие, не делая ничего другого.<exception_translator<>>просто возвращает исходное исключение, если обработка исключения была неудачной. Так же, как и при простом обращении с исключениями, в этом случае охрана прицела внутри<state_machine<>>обеспечивает уничтожение всех государственных объектов до того, как клиент поймает исключение.
Which states can react to an exception_thrown event?
Короткий ответ: Если машина состояния стабильна при забрасывании исключения, то состояние, вызвавшее исключение, сначала пробуют на реакцию. В противном случае для реакции сначала пробуют внешнеенеустойчивое состояние.
Более длинный ответ: Существует три сценария:
Функция членаreactраспространяет исключениедо того, каквызовет любую из реакционных функций, или действие, выполняемое во время реакции в состоянии, распространяет исключение. Состояние, вызвавшее исключение, сначала пробуют на реакцию, поэтому после получения события EvStart к Дефекту перейдет следующая машина:
Вступительное действие государства (конструктор) распространяет исключение:
Если ортогональных областей нет, то непосредственное внешнее состояние состояния, вызвавшего исключение, сначала пробуют на реакцию, поэтому следующая машина после попытки войти Остановится перейдет в Дефектное:
Если есть ортогональные области, то для реакции сначала пробуют внешнеенеустойчивое состояние. Внешнее нестабильное состояние обнаруживается путем первого выбора прямого внешнего состояния состояния, вызвавшего исключение, а затем перемещения наружу до тех пор, пока не будет найдено состояние, которое нестабильно, но не имеет прямых или косвенных внешних состояний, которые нестабильны. Это более сложное правило необходимо, потому что только реакции, связанные с внешним неустойчивым состоянием (или любым из его прямых или косвенных внешних состояний), способны вернуть машину в стабильное состояние. Рассмотрим следующую диаграмму состояний:
Будет ли эта машина состояний в конечном итоге переходить к E или F после инициации, зависит от того, какая из двух ортогональных областей инициирована первой. Если сначала инициируется верхняя ортогональная область, последовательность входа выглядит следующим образом: A, D, B (исключение). Как D, так и B были успешно введены, поэтому B является самым внешним нестабильным состоянием, когда выбрасывается исключение, и поэтому машина перейдет к F. Однако, если сначала инициируется нижняя ортогональная область, последовательность выглядит следующим образом: A, B (исключение). D никогда не вводился, поэтому A является самым внешним нестабильным состоянием, когда выбрасывается исключение, и поэтому машина переходит к E. На практике эти различия редко имеют значение, поскольку восстановление ошибок верхнего уровня является адекватным для большинства государственных машин. Однако, поскольку последовательность инициации четко определена (первоначально всегда инициируется ортогональная область 0, затем область 1 и так далее), пользователимогутточно контролировать, когда и где они хотят обрабатывать исключения .
Переходное действие распространяет исключение: Наиболее распространенное внешнее состояние источника и целевое состояние сначала испытывают на реакцию, поэтому после получения события EvStartStop следующая машина перейдет в Дефект:
Как и при нормальном событии, алгоритм отправки будет двигаться наружу, чтобы найти реакцию, если первое испытанное состояние не обеспечивает ее (или если реакция явно возвращается<forward_event();>). Однаков отличие от обычных событий, он сдастся, как только безуспешно опробует внешнее состояние, поэтому следующая машинанеперейдет в Дефект после получения события EvNumLockPressed:
Вместо этого машина прекращается, и первоначальное исключение перебрасывается.
Поскольку событие<exception_thrown>отправляется из блока улова, мы можем перебросить и поймать исключение в пользовательской реакции:
struct Defective : sc::simple_state<
Defective, Purifier > {};
// Pretend this is a state deeply nested in the Purifier
// state machine
struct Idle : sc::simple_state< Idle, Purifier >
{
typedef mpl::list<
sc::custom_reaction< EvStart >,
sc::custom_reaction< sc::exception_thrown >
> reactions;
sc::result react( const EvStart & )
{
throw std::runtime_error( "" );
}
sc::result react( const sc::exception_thrown & )
{
try
{
throw;
}
catch ( const std::runtime_error & )
{
// only std::runtime_errors will lead to a transition
// to Defective ...
return transit< Defective >();
}
catch ( ... )
{
// ... all other exceptions are forwarded to our outer
// state(s). The state machine is terminated and the
// exception rethrown if the outer state(s) can't
// handle it either...
return forward_event();
}
// Alternatively, if we want to terminate the machine
// immediately, we can also either rethrow or throw
// a different exception.
}
};
К сожалению, эта идиома (используя<throw;>внутри блока<try>, вложенного внутри блока<catch>) не работает по крайней мере на одном очень популярном компиляторе.Если вам нужно использовать одну из этих платформ, вы можете перейти к шаблону класса<state_machine>. Это позволит вам генерировать различные события в зависимости от типа исключения.
Если подтип<simple_state<>>или<state<>>объявляет функцию публичного члена с подписью<void
exit()>, то эта функция называется непосредственно перед уничтожением государственного объекта. Как объясняется вОбработка ошибокв Rationale, это полезно для двух вещей, которые в противном случае было бы трудно или громоздко достичь только с помощью деструкторов:
Чтобы сигнализировать о сбое в действиях выхода
Выполнять определенные действия выходатолькопри переходе или прекращении, но не при разрушении объекта государственной машины.
Несколько моментов, которые следует учитывать перед использованием<exit()>:
Нет гарантии, чтоexit()будет называться:
Если клиент уничтожает объект машины состояния без вызоваterminate()заранее, то активные в настоящее время состояния уничтожаются без вызоваexit(). Это необходимо, потому что исключение, которое может быть выброшено изexit(), не может быть распространено на клиента государственной машины.
exit()не называется, когда ранее выполненное действие распространяло исключение, и это исключение (пока) не было успешно обработано. Это связано с тем, что новое исключение, которое может быть выброшено изexit(), маскирует первоначальное исключение.
Государство считается выбывшим, даже если егоexitфункция распространяла исключение. То есть государственный объект неизбежно разрушается сразу после вызоваexit(), независимо от того, распространялось лиexit()исключение или нет. Поэтому государственная машина, сконфигурированная для расширенной обработки исключений, всегда нестабильна при обработке исключений, распространяемых из функцииexit.
В машине состояния, сконфигурированной для расширенной обработки исключений, правила обработки для события исключения, возникающего в результате исключения, распространяемого изexit(), аналогичны тем, которые определены для исключений, распространяемых от государственных конструкторов. То есть, самое внешнее нестабильное состояние сначала испробовано на реакцию, и диспетчер затем перемещается наружу, пока не будет найдена соответствующая реакция
Субмашины для программирования на основе событий — это функции для процедурного программирования, многоразовые строительные блоки, реализующие часто необходимую функциональность. Связанная с этим нотация UML не совсем ясна для меня. Кажется, что она сильно ограничена (например, одна и та же субмашина не может появиться в разных ортогональных областях) и, по-видимому, не учитывает очевидные вещи, такие как параметры.
Повышаю. Statechart совершенно не знает о подмашинах, но они могут быть реализованы довольно хорошо с шаблонами. Здесь используется подмашина для улучшения реализации копировальной пасты клавиатурной машины, обсуждаемой вортогональных состояниях:
Как следует из названия, машина синхронного состояния обрабатывает каждое событие синхронно. Это поведение реализуется шаблоном класса<state_machine>, функция которого<process_event>возвращается только после выполнения всех реакций (в том числе тех, которые вызваны внутренними событиями, которые могли быть размещены действиями). Эта функция является строго не входящим (так же, как и все другие функции-члены, поэтому<state_machine<>>не является безвредной для потока). Это затрудняет правильное общение двух объектов подтипа<state_machine<>>посредством событий двунаправленным образомдаже в однопоточной программе. Например, машина состояний<A>находится в середине обработки внешнего события. Внутри действия он решает отправить новое событие в государственную машину<B>(позвонив<B::process_event()>). Затем он «ждет», чтобы B отправил ответ через<boost::function<>>— как обратный вызов, который ссылается на<A::process_event()>и был передан в качестве участника данных события. Однако, в то время как<A>«ждет»<B>, чтобы отправить обратно событие,<A::process_event()>еще не вернулся от обработки внешнего события и как только<B>отвечает через обратный вызов,<A::process_event()>неизбежновозвращается. Все это действительно происходит в одной нити, поэтому «подождите» в кавычках.
How it works
Шаблон класса<asynchronous_state_machine>не имеет ни одной из функций-членов шаблона класса<state_machine>. Более того, 180 объектов подтипа не могут быть созданы или уничтожены напрямую. Вместо этого все эти операции должны выполняться через объект<Scheduler>, с которым связана каждая машина асинхронного состояния. Все эти<Scheduler>функции-члены только толкают соответствующий элемент в очередь планировщиков, а затем немедленно возвращаются. Выделенный поток позже вытащит предметы из очереди, чтобы они были обработаны.
Приложения обычно сначала создают объект<fifo_scheduler<>>, а затем вызывают<fifo_scheduler<>::create_processor<>()>и<fifo_scheduler<>::initiate_processor()>для планирования создания и инициирования одного или более объектов подтипа<asynchronous_state_machine<>>. Наконец,<fifo_scheduler<>::operator()()>либо вызывается непосредственно, чтобы позволить машине(ам) работать в текущей нити, либо<boost::function<>>объект, ссылающийся<operator()()>, передается новому<boost::thread>. В качестве альтернативы, последнее также может быть сделано сразу после строительства объекта<fifo_scheduler<>>. В следующем коде мы запускаем одну машину состояния в новом<boost::thread>, а другую в основном потоке (см. пример PingPong для полного исходного кода):
struct Waiting;
struct Player :
sc::asynchronous_state_machine< Player, Waiting >
{
// ...
};
// ...
int main()
{
// Create two schedulers that will wait for new events
// when their event queue runs empty
sc::fifo_scheduler<> scheduler1( true );
sc::fifo_scheduler<> scheduler2( true );
// Each player is serviced by its own scheduler
sc::fifo_scheduler<>::processor_handle player1 =
scheduler1.create_processor< Player >( /* ... */ );
scheduler1.initiate_processor( player1 );
sc::fifo_scheduler<>::processor_handle player2 =
scheduler2.create_processor< Player >( /* ... */ );
scheduler2.initiate_processor( player2 );
// the initial event that will start the game
boost::intrusive_ptr< BallReturned > pInitialBall =
new BallReturned();
// ...
scheduler2.queue_event( player2, pInitialBall );
// ...
// Up until here no state machines exist yet. They
// will be created when operator()() is called
// Run first scheduler in a new thread
boost::thread otherThread( boost::bind(
&sc::fifo_scheduler<>::operator(), &scheduler1, 0 ) );
scheduler2(); // Run second scheduler in this thread
otherThread.join();
return 0;
}
Мы могли бы также использовать два повышения::threads:
Во всех приведенных выше примерах<fifo_scheduler<>::operator()()>ожидает пустой очереди событий и вернется только после вызова<fifo_scheduler<>::terminate()>. Машина состояния<Player>называет эту функцию на своем объекте планировщика непосредственно перед окончанием.
Пересмотрено03 Декабря 200603 December, 2006[ORIG_END] -->
Статья The Boost Statechart Library - Tutorial раздела может быть полезна для разработчиков на c++ и boost.
Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.