struct Active;
struct Stopped;
struct Running;
struct StopWatch : sc::state_machine< StopWatch, Active >
{
// startTime_ remains uninitialized, because there is no reasonable default
StopWatch() : elapsedTime_( 0.0 ) {}
~StopWatch()
{
terminate();
}
double ElapsedTime() const
{
// Ugly switch over the current state.
if ( state_cast< const Stopped * >() != 0 )
{
return elapsedTime_;
}
else if ( state_cast< const Running * >() != 0 )
{
return elapsedTime_ + std::difftime( std::time( 0 ), startTime_ );
}
else // we're terminated
{
throw std::bad_cast();
}
}
// elapsedTime_ is only meaningful when the machine is not terminated
double elapsedTime_;
// startTime_ is only meaningful when the machine is in Running
std::time_t startTime_;
};
struct Active : sc::state< Active, StopWatch, Stopped >
{
typedef sc::transition< EvReset, Active > reactions;
Active( my_context ctx ) : my_base( ctx )
{
outermost_context().elapsedTime_ = 0.0;
}
};
struct Running : sc::state< Running, Active >
{
typedef sc::transition< EvStartStop, Stopped > reactions;
Running( my_context ctx ) : my_base( ctx )
{
outermost_context().startTime_ = std::time( 0 );
}
~Running()
{
outermost_context().elapsedTime_ +=
std::difftime( std::time( 0 ), outermost_context().startTime_ );
}
};
struct Stopped : sc::simple_state< Stopped, Active >
{
typedef sc::transition< EvStartStop, Running > reactions;
};
Этот StopWatch не использует государственно-локальное хранилище при реализации того же поведения, что и учебник StopWatch. Хотя этот код, вероятно, легче читать для неподготовленного глаза, у него есть несколько проблем, которые отсутствуют в оригинале:
В этом классе StopWatch есть члены данных, которые имеют значимую ценность только в том случае, если машина состояния находится в определенном состоянии. То есть время жизни этих переменных не совпадает с временем жизни объекта StopWatch, содержащего их. Поскольку время жизни управляется действиями государств входа и выхода, нам нужно использовать уродливый переключатель по текущему состоянию, если мы хотим получить к ним доступ из контекста, где текущее состояние неясно. Это, по сути, дублирует некоторую государственную логику FSM. Поэтому всякий раз, когда нам нужно изменить макет государственной машины, нам, вероятно, также нужно будет изменить уродливый выключатель. Хуже того, если мы забудем изменить выключатель, код, вероятно, все равно будет компилироваться и, возможно, даже молча делать неправильные вещи. Обратите внимание, что это невозможно с версией в учебном пособии, которая хотя бы забросит исключение и часто просто откажется от компиляции. Более того, для учебника StopWatch гораздо выше вероятность того, что программист впервые исправит изменение, поскольку код, который вычисляет прошедшее время, расположен близко к коду, который обновляет переменные.
Мы должны менять класс StopWatch всякий раз, когда хотим ввести новую переменную или изменить тип уже существующей переменной. То есть многие изменения в FSM, скорее всего, приведут к изменению класса StopWatch. Во всех FSM, которые не используют государственное локальное хранилище, подтипstate_machine<>, таким образом, будет точкой изменения, которая является довольно надежным индикатором для плохого дизайна.
Оба пункта не представляют большой проблемы в таком небольшом примере, который может быть легко реализован в одном блоке перевода одним программистом. Тем не менее, они быстро становятся серьезной проблемой для большой сложной машины, распределенной по нескольким блокам перевода, которые, возможно, даже поддерживаются различными программистами.
Чтобы понять, почему и как это возможно, важно вспомнить следующие факты:
Функции-члены шаблона класса C++ инстанцируются в точке, где они фактически называются. Если функция никогда не называется, она не будет реализована и не будет сформирована ни одна инструкция по сборке.
ПараметрInitialStateшаблонаsc::state_machineможет быть неполным типом (т.е. объявленным вперед).
Функция<state_machine<>::initiate()>создает объект начального состояния. Таким образом, определение этого состояния должно быть известно до того, как компилятор достигнет точки, где<initiate()>называется. Чтобы скрыть начальное состояние машины в файле .cpp, мы больше не должны позволять клиентам звонить<initiate()>. Вместо этого мы делаем это в файле .cpp, в точке, где известно полное определение начального состояния.
Пример:
StopWatch.hpp:
// define events ...
struct Active; // the only visible forward
struct StopWatch : sc::state_machine< StopWatch, Active >
{
StopWatch();
};
StopWatch.cpp:
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;
};
StopWatch::StopWatch()
{
// For example, we might want to ensure that the state
// machine is already started after construction.
// Alternatively, we could add our own initiate() function
// to StopWatch and call the base class initiate() in the
// implementation.
initiate();
}
Пример PingPong показывает, как работает асинхронная машина<>Подкласс может быть скрыт.
Да, но вопреки тому, что позволяют некоторые генераторы кода FSM, Boost. Машины Statechart могут делать это только так, как это было предусмотрено конструктором базовой государственной машины:
Библиотека была создана до появления 2.0. Поэтому, если не указано иное, библиотека реализует поведение, предписанное стандартом UML1.5. Вот неполный список различий между семантикой 2.0 и усилием. Семантика Statechart:
Все переходы всегда внешние. Местные переходы вообще не поддерживаются. К сожалению, спецификации UML2.0 не совсем ясно, как должны работать локальные переходы, см.здесьдля получения дополнительной информации.
Прямая поддержка точки входа и выхода элементов UML2.0 отсутствует. Тем не менее, оба могут быть легко смоделированы, первый с типдефом, а второй с состоянием, которое является шаблоном (с переходом назначения в качестве параметра шаблона).
Проблема возникает потому, что<state_machine<>::~state_machine>неизбежно разрушает все оставшиеся активные состояния. В настоящее время<Machine::~Machine>уже запущено, что делает незаконным доступ к любому из<Machine>членов. Эту проблему можно избежать, определив следующий деструктор:
Это зависит. Как объясняется вКомпромиссы скорости и масштабируемостина странице Производительность, практически безграничная масштабируемость, предлагаемая этой библиотекой, имеет свою цену. Особенно маленькие и простые FSM могут быть легко реализованы, так что они потребляют меньше циклов и меньше памяти и занимают меньше кодового пространства в исполняемом файле. Вот некоторые очевидныеочень грубыеоценки:
Для машины состояния с максимум одним одновременно активным состоянием (то есть машина плоская и не имеет ортогональных областей) с тривиальными действиями, настраиваемым управлением памятью и компилируемым с хорошим оптимизирующим компилятором процессор класса Pentium 4 не должен проводить внутри более 1000 цикловstate_machine<>::process_event(). Это наихудшее время для обработки одного события более или менее линейно с количеством одновременно активных состояний для более сложных машин состояний, при этом типичное среднее значение намного ниже этого. Таким образом, довольно сложная машина с максимум 10 одновременно активными состояниями, работающими на процессоре с частотой 100 МГц, должна обрабатывать более 10 000 событий в секунду.
Один объект машины обычно использует менее 1 КБ памяти, даже если он реализует очень сложную машину.
Для размера кода трудно дать конкретное руководство, но тесты на примере BitMachine показывают, что размер кода масштабируется более или менее линейно с количеством состояний (переходы, по-видимому, оказывают лишь незначительное влияние). При компиляции с MSVC7.1 в Windows 32 состояния и 224 перехода, по-видимому, вписываются в исполняемый код ~ 108 КБ (при включенной оптимизации). Кроме того, библиотека может быть скомпилирована с C++ RTTI и отключена обработка исключений, что приводит к значительной экономии на большинстве платформ.
Как упоминалось выше, это очень приблизительные оценки, полученные из использования библиотеки на настольном ПК, поэтому их следует использовать только для того, чтобы решить, есть ли смысл в проведении собственных тестов производительности на вашей целевой платформе.
Да. Из коробки единственными операциями, требующими потенциально недетерминированного времени, которое выполняет библиотека, являются вызовы<std::allocator<>>функций-членов и<dynamic_cast>s.<std::allocator<>>вызовы функций-членов можно избежать, передав пользовательский распределитель<event<>>,<state_machine<>>,<asynchronous_state_machine<>>,<fifo_scheduler<>>и<fifo_worker<>>.<dynamic_cast>s можно избежать, не называя<state_cast<>>функции-члены<state_machine<>>,<simple_state<>>и<state<>>, а используя детерминированный вариант<state_downcast<>>.
#include <boost/statechart/state_machine.hpp>
#include <boost/statechart/simple_state.hpp>
namespace sc = boost::statechart;
template< typename X > struct A;
struct Machine : sc::state_machine< Machine, A< int > > {};
template< typename X > struct B;
template< typename X >
struct A : sc::simple_state< A< X >, Machine, B< X > > {};
template< typename X >
struct B : sc::simple_state< B< X >, A< X > > {};
int main()
{
Machine machine;
machine.initiate();
return 0;
}
Если шаблоны<A>и<B>заменены обычными типами, вышеупомянутый код компилируется без ошибок. Это связано с тем, что C++ рассматривает шаблоны, объявленные вперед, иначе, чем типы объявленных вперед. А именно, компилятор пытается получить доступ к файлам типа<B< X >>в точке, где шаблон еще не определен. К счастью, этого можно легко избежать, поместив все внутренние аргументы начального состояния в<mpl::list<>>.
struct A : sc::simple_state<
A< X >, Machine, mpl::list< B< X > > > {};
Наверное, нет. Существует несколько возможных причин таких ошибок компиляции:
Ваш компилятор слишком багги, чтобы компилировать библиотеку, см.здесьдля информации о состоянии вашего компилятора. Если вы обязательно должны использовать такой компилятор для своего проекта, боюсь, Boost. Statechart не для вас.
Ошибка сообщается на строке, аналогичной следующей:
Скорее всего, в вашем коде есть ошибка. В библиотеке есть много таких утверждений о времени компиляции, чтобы гарантировать, что недействительные машины состояний не могут быть скомпилированы (для представления о том, какие типы ошибок сообщаются во время компиляции, см. тесты отказа компиляции). Над каждым из этих утверждений есть комментарий, объясняющий проблему. Почти на всех современных компиляторах ошибка в коде шаблона сопровождается текущим «стеком обоснования». Очень похоже на стек вызовов, который вы видите в отладчике, этот «стек обоснования» позволяет вам отслеживать ошибку через инстанциации библиотечного кода, пока вы не нажмете на строку своего кода, которая вызывает проблему. Например, вот сообщение об ошибке MSVC7.1 для кода в InconsistentHistoryTest1.cpp:
...\boost\statechart\shallow_history.hpp(34) : error C2027: use of undefined type 'boost::STATIC_ASSERTION_FAILURE<x>'
with
[
x=false
]
...\boost\statechart\shallow_history.hpp(34) : see reference to class template instantiation 'boost::STATIC_ASSERTION_FAILURE<x>' being compiled
with
[
x=false
]
...\boost\statechart\simple_state.hpp(861) : see reference to class template instantiation 'boost::statechart::shallow_history<DefaultState>' being compiled
with
[
DefaultState=B
]
...\boost\statechart\simple_state.hpp(599) : see reference to function template instantiation 'void boost::statechart::simple_state<MostDerived,Context,InnerInitial>::deep_construct_inner_impl_non_empty::deep_construct_inner_impl<InnerList>(const boost::statechart::simple_state<MostDerived,Context,InnerInitial>::inner_context_ptr_type &,boost::statechart::simple_state<MostDerived,Context,InnerInitial>::outermost_context_base_type &)' being compiled
with
[
MostDerived=A,
Context=InconsistentHistoryTest,
InnerInitial=boost::mpl::list<boost::statechart::shallow_history<B>>,
InnerList=boost::statechart::simple_state<A,InconsistentHistoryTest,boost::mpl::list<boost::statechart::shallow_history<B>>>::inner_initial_list
]
...\boost\statechart\simple_state.hpp(567) : see reference to function template instantiation 'void boost::statechart::simple_state<MostDerived,Context,InnerInitial>::deep_construct_inner<boost::statechart::simple_state<MostDerived,Context,InnerInitial>::inner_initial_list>(const boost::statechart::simple_state<MostDerived,Context,InnerInitial>::inner_context_ptr_type &,boost::statechart::simple_state<MostDerived,Context,InnerInitial>::outermost_context_base_type &)' being compiled
with
[
MostDerived=A,
Context=InconsistentHistoryTest,
InnerInitial=boost::mpl::list<boost::statechart::shallow_history<B>>
]
...\boost\statechart\simple_state.hpp(563) : while compiling class-template member function 'void boost::statechart::simple_state<MostDerived,Context,InnerInitial>::deep_construct(const boost::statechart::simple_state<MostDerived,Context,InnerInitial>::context_ptr_type & ,boost::statechart::simple_state<MostDerived,Context,InnerInitial>::outermost_context_base_type &)'
with
[
MostDerived=A,
Context=InconsistentHistoryTest,
InnerInitial=boost::mpl::list<boost::statechart::shallow_history<B>>
]
...\libs\statechart\test\InconsistentHistoryTest1.cpp(29) : see reference to class template instantiation 'boost::statechart::simple_state<MostDerived,Context,InnerInitial>' being compiled
with
[
MostDerived=A,
Context=InconsistentHistoryTest,
InnerInitial=boost::mpl::list<boost::statechart::shallow_history<B>>
]
В зависимости от используемой вами IDE возможно, что вам нужно переключиться на другое окно, чтобы увидеть это полное сообщение об ошибке (например, для Visual Studio 2003 вам нужно переключиться на окно вывода). Начав сверху и спустившись по списку инстанциаций, вы видите, что каждая из них сопровождается именем файла и номером строки. Игнорируя все файлы, принадлежащие библиотеке, мы находим виновника ближе к низу в файле InconsistentHistoryTest1.cpp на строке 29.
Ошибка сообщается на строке, расположенной далеко от BOOST_. STATIC_ASSERT. Используйте технику, описанную в пункте 2, чтобы увидеть, какая строка вашего кода вызывает проблему. Если ваш код правильный, вы нашли ошибку в компиляторе или Boost. Государственная карта. Пожалуйста,отправьте мненебольшую, но полную программу, показывающую проблему. Спасибо!
Невидимая для пользователя библиотека использует статические элементы данных для реализации собственного оптимизированного по скорости RTTI-механизма для подтипов<event<>>и<simple_state<>>. Всякий раз, когда такой подтип определяется в файле заголовка и затем включается в несколько TU, линкер позже должен устранить дублирующие определения статических элементов данных. Обычно это работает безупречно, пока все эти ТУстатическисвязаны в одну двоичную. Это намного сложнее, когда речь идет о DLL. Файлы TuTest*.?pp иллюстрируют это:
TuTest.hpp: Обосновывает шаблон класса, содержащий статический элемент данных
TuTest.cpp: Включает TuTest.hpp и компилируется в DLL
TuTestMain.cpp: Включает TuTest.hpp и компилируется в исполняемый файл
Без каких-либо мер предосторожности (например,<__declspec(dllexport)>на компиляторах, совместимых с MSVC), на большинстве платформ оба двоичных файла (exe & dll) теперь содержат свой собственный экземпляр статического элемента данных. Поскольку механизм RTTI предполагает, что во время выполнения есть ровно один объект этого элемента, механизм эффектно выходит из строя, когда процесс, запущенный exe, также загружает dll. Различные платформы по-разному решают эту проблему:
На некоторых платформах (например, MinGW) просто не существует способа обеспечить существование такого участника только один раз во время выполнения. Следовательно, внутренний механизм RTTI не может надежно использоваться в сочетании с DLL. Отключение его путем определенияBOOST_STATECHART_USE_NATIVE_RTTIво всех ТУ будетобычноработать вокруг проблемы.
MSVC-совместимые компиляторы поддерживают__declspec(dllimport)и__declspec(dllexport), что позволяет точно определить, что нужно загружать из DLL (см. пример TuTest, как это сделать). Таким образом, можно использовать внутренний механизм RTTI, но необходимо позаботиться о том, чтобы правильно экспортировать и импортировать всеevent<>иsimple_state<>подтипы, определенные в заголовках, которые скомпилированы в более чем один двоичный код. Альтернативно, конечноBOOST_STATECHART_USE_NATIVE_RTTIможет также использоваться для экономии работы по импорту и экспорту.
Нет. Хотя события могут быть выведены друг из друга для написания общего кода только один раз,реакциимогут быть определены только для большинства происходящих событий.
Пример:
template< class MostDerived >
struct EvButtonPressed : sc::event< MostDerived >
{
// common code
};
struct EvPlayButtonPressed :
EvButtonPressed< EvPlayButtonPressed > {};
struct EvStopButtonPressed :
EvButtonPressed< EvStopButtonPressed > {};
struct EvForwardButtonPressed :
EvButtonPressed< EvForwardButtonPressed > {};
/* ... */
// We want to turn the player on, no matter what button we
// press in the Off state. Although we can write the reaction
// code only once, we must mention all most-derived events in
// the reaction list.
struct Off : sc::simple_state< Off, Mp3Player >
{
typedef mpl::list<
sc::custom_reaction< EvPlayButtonPressed >,
sc::custom_reaction< EvStopButtonPressed >,
sc::custom_reaction< EvForwardButtonPressed >
> reactions;
template< class MostDerived >
sc::result react( const EvButtonPressed< MostDerived > & )
{
// ...
}
};
Обновление: Реализация в этой области существенно изменилась. До сих пор можно получить такое поведение при редких обстоятельствах (когда действие распространяет исключение в машине состояний с ортогональными областямии, если макет диаграммы состояний удовлетворяет определенным условиям), но оно больше не может быть продемонстрировано с помощью примерной программы ниже. Тем не менее, описанный обходной путь все еще действителен и гарантирует, что это поведение никогда не появится.
Они определенно не для подтипов<simple_state<>>и<state<>>, но деструкторы дополнительных оснований могут быть названы в строительном порядке (а не в обратном порядке строительства):
То есть часть<EntryExitDisplayer>базового класса<Outer>разрушается до<Inner>, хотя<Inner::~Inner()>называется до<Outer::~Outer()>. Это несколько неинтуитивное поведение вызвано следующими фактами:
simple_state<>часть базового классаInnerотвечает за уничтожениеOuter
Деструкторы частей базового класса называются в обратном порядке строительства
Так, когда<Outer>деструктор называется call stack выглядит следующим образом:
Заметим, что<Inner::~Inner()>еще не имел возможности уничтожить свою часть<EntryExitDisplayer>базового класса, так как сначала ее приходится называть разрушителемвторогобазового класса. Теперь<Outer::~Outer()>сначала уничтожит свою часть<simple_state<
Outer, ... >>базового класса, а затем сделает то же самое со своей частью<EntryExitDisplayer>базового класса. Затем стек возвращается к<Inner::~Inner()>, который затем может, наконец, закончиться звонком<EntryExitDisplayer::~EntryExitDisplayer()>.
К счастью, есть простой обходной путь: Пусть<simple_state<>>и<state<>>всегда будут первым базовым классом государства. Это гарантирует, что деструкторы дополнительных оснований вызываются до рекурсии, используемые деструкторами государственных баз могут изменить порядок разрушения.
Статья The Boost Statechart Library - FAQ раздела может быть полезна для разработчиков на c++ и boost.
Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.