|
The Boost Statechart LibraryFrequently Asked Questions (FAQs) |
No. Although events can be derived from each other to write common code only once, reactions can only be defined for most-derived events.
Example:
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, mpl::list< sc::custom_reaction< EvPlayButtonPressed >, sc::custom_reaction< EvStopButtonPressed >, sc::custom_reaction< EvForwardButtonPressed > > > { template< class MostDerived > sc::result react( const EvButtonPressed< MostDerived > & ) { // ... } };
To see why and how this is possible it is important to recall the following facts:
InitialState
template parameter of
sc::state_machine
can be an incomplete type (i.e. forward declared).The class template member function state_machine<>::initiate()
creates an object of the initial state. So, the definition of this state must
be known before the compiler reaches the point where initiate()
is called. To be able to hide the initial state of a state machine in a .cpp
file we must therefore no longer let clients call initiate()
.
Instead, we do so in the .cpp file, at a point where the full definition of
the initial state is known.
Example:
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, sc::transition< EvReset, Active >, Stopped > {}; struct Running : sc::simple_state< Running, Active, sc::transition< EvStartStop, Stopped > > {}; struct Stopped : sc::simple_state< Stopped, Active, sc::transition< EvStartStop, Running > > {}; StopWatch::StopWatch() { // For example, we might want to ensure that the state // machine is already started after construction. // Alternatively we could also provide the client with // a Start() function... initiate(); }
Yes, but contrary to what some FSM code generators allow, Boost.Statechart machines can do so only in a way that was foreseen by the designer of the base state machine:
struct EvStart : sc::event< EvStart > {}; struct Idle; struct PumpBase : sc::state_machine< PumpBase, Idle > { virtual sc::result react( Idle & idle, const EvStart & ) const; }; struct Idle : sc::simple_state< Idle, PumpBase, sc::custom_reaction< EvStart > > { sc::result react( const EvStart & evt ) { return context< PumpBase >().react( *this, evt ); } }; struct Running : sc::simple_state< Running, PumpBase > {}; sc::result PumpBase::react( Idle & idle, const EvStart & ) const { return idle.transit< Running >(); } struct MyRunning : sc::simple_state< MyRunning, PumpBase > {}; struct MyPump : PumpBase { virtual sc::result react( Idle & idle, const EvStart & ) const { return idle.transit< MyRunning >(); } };
Update: The implementation has changed considerably in this area. It is still possible to get this behavior under rare circumstances (when an action propagates an exception in a state machine with orthogonal regions and if the statechart layout satisfies certain conditions), but it can no longer be demonstrated with the example program below. However, the described workaround is still valid and ensures that this behavior will never show up.
They definitely aren't for the simple_state
and state
subclasses, but the destructors of additional bases might be called in
construction order (rather than the reverse construction order):
#include <boost/statechart/state_machine.hpp> #include <boost/statechart/simple_state.hpp> namespace sc = boost::statechart; class EntryExitDisplayer { protected: EntryExitDisplayer( const char * pName ) : pName_( pName ) { std::cout << pName_ << " entered\n"; } ~EntryExitDisplayer() { std::cout << pName_ << " exited\n"; } private: const char * const pName_; }; struct Outer; struct Machine : sc::state_machine< Machine, Outer > {}; struct Inner; struct Outer : EntryExitDisplayer, sc::simple_state< Outer, Machine, sc::no_reactions, Inner > { Outer() : EntryExitDisplayer( "Outer" ) {} }; struct Inner : EntryExitDisplayer, sc::simple_state< Inner, Outer > { Inner() : EntryExitDisplayer( "Inner" ) {} }; int main() { Machine myMachine; myMachine.initiate(); return 0; }
This program will produce the following output:
Outer entered Inner entered Outer exited Inner exited
That is, the EntryExitDisplayer
base class portion of
Outer
is destructed before the one of Inner
although
Inner::~Inner()
is called before Outer::~Outer()
. This
somewhat counter-intuitive behavior is caused by the following facts:
simple_state<>
base class portion of Inner
is responsible to destruct Outer
So, when the Outer
destructor is called the call stack looks as
follows:
Outer::~Outer() simple_state< Inner, ... >::~simple_state() Inner::~Inner()
Note that Inner::~Inner()
did not yet have a chance to destroy
its EntryExitDisplayer
base class portion, as it first has to call
the destructor of the second base class. Now Outer::~Outer()
will first destruct its simple_state< Outer, ... >
base class
portion and then do the same with its EntryExitDisplayer
base class
portion. The stack then unwinds back to Inner::~Inner()
, which can
then finally finish by calling
EntryExitDisplayer::~EntryExitDisplayer()
.
Luckily, there is an easy work-around: Always let simple_state<>
and state<>
be the first base class of a state. This ensures that
destructors of additional bases are called before recursion employed by state
base destructors can alter the order of destruction.
It depends. As explained under Speed versus scalability tradeoffs in the Rationale, the virtually limitless scalability offered by this library does have its price. Especially small and simple FSMs can easily be implemented so that they consume fewer cycles and less memory and occupy less code space in the executable. Here are some obviously very rough estimates:
state_machine<>::process_event()
. This worst-case
dispatch and transition time scales more or less linearly with the number of
simultaneously active states for more complex state machines, with the
typical average being much lower than that. So, a fairly complex machine
with at most 10 simultaneously active states running on a 100MHz CPU should
be able to process more than 10'000 events per secondAs mentioned above, these are very rough estimates derived from the use of the library on a desktop PC, so they should only be used to decide whether there is a point in making your own performance tests on your target platform.
Yes. Out of the box, the only operations taking potentially non-deterministic
time that the library performs are calls to
global operator new
and dynamic_cast
s. Global
operator new
calls can be avoided by passing a custom allocator to
state_machine<>
and by giving all state classes a custom
operator new
. dynamic_cast
s can be avoided by not calling the
state_cast<>
member functions of state_machine<>
,
simple_state<>
and state<>
but using the
deterministic variant state_downcast<>
instead.
Revised 10 May, 2005
© Copyright Andreas Huber Dönni 2003-2005. The link refers to a spam honeypot. Please remove the words spam and trap to obtain my real address.
Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)