...one of the most highly
regarded and expertly designed C++ library projects in the
world.
— Herb Sutter and Andrei
Alexandrescu, C++
Coding Standards
This is best explained with an example:
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; };
This StopWatch does not make any use of state-local storage while implementing the same behavior as the tutorial StopWatch. Even though this code is probably easier to read for the untrained eye, it does have a few problems that are absent in the original:
StopWatch::ElapsedTime()
) if we want to
access them from a context where the current state is unclear. This
essentially duplicates some of the state logic of the FSM. Therefore,
whenever we need to change the layout of the state machine we will likely
also need to change the ugly switch. Even worse, if we forget to change
the switch, the code will probably still compile and maybe even silently
do the wrong thing. Note that this is impossible with the version in the
tutorial, which will at least throw an exception and often just refuse to
compile. Moreover, for the tutorial StopWatch there's a much higher
chance that a programmer will get a change correct the first time since
the code that calculates the elapsed time is located close to the code
that updates the variablesstate_machine<>
subtype will therefore be a change
hotspot, which is a pretty sure indicator for a bad designBoth points are not much of a problem in a small example like this, which can easily be implemented in a single translation unit by a single programmer. However, they quickly become a major problem for a big complex machine spread over multiple translation units, which are possibly even maintained by different programmers.
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, 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(); }
The PingPong example demonstrates how the inner workings of an asynchronous_state_machine<> subclass can be hidden.
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 > { typedef sc::custom_reaction< EvStart > reactions; 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 >(); } };
The library was designed before 2.0 came along. Therefore, if not explicitly noted otherwise, the library implements the behavior mandated by the UML1.5 standard. Here's an incomplete list of differences between the 2.0 semantics & Boost.Statechart semantics:
When compiled with NDEBUG
undefined, running the following
program results in a failed assert:
#include <boost/statechart/state_machine.hpp> #include <boost/statechart/simple_state.hpp> #include <iostream> struct Initial; struct Machine : boost::statechart::state_machine< Machine, Initial > { Machine() { someMember_ = 42; } int someMember_; }; struct Initial : boost::statechart::simple_state< Initial, Machine > { ~Initial() { std::cout << outermost_context().someMember_; } }; int main() { Machine().initiate(); return 0; }
The problem arises because state_machine<>::~state_machine
inevitably destructs all remaining active states. At this time,
Machine::~Machine
has already been run, making it illegal to
access any of the Machine
members. This problem can be avoided
by defining the following destructor:
~Machine() { terminate(); }
It depends. As explained under Speed versus scalability tradeoffs on the Performance page, 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 time to process one event 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
std::allocator<>
member functions and
dynamic_cast
s. std::allocator<>
member
function calls can be avoided by passing a custom allocator to
event<>
, state_machine<>
,
asynchronous_state_machine<>
,
fifo_scheduler<>
and fifo_worker<>
.
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.
The following code generates such an error:
#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; }
If the templates A
and B
are replaced with
normal types, the above code compiles without errors. This is rooted in the
fact that C++ treats forward-declared templates differently than
forward-declared types. Namely, the compiler tries to access member
typedefs of B< X >
at a point where the template has not
yet been defined. Luckily, this can easily be avoided by putting all inner
initial state arguments in an mpl::list<>
, as
follows:
struct A : sc::simple_state< A< X >, Machine, mpl::list< B< X > > > {};
See this post for technical details.
Probably not. There are several possible reasons for such compile-time errors:
BOOST_STATIC_ASSERT( ( mpl::less< orthogonal_position, typename context_type::no_of_orthogonal_regions >::value ) );Most probably, there is an error in your code. The library has many such compile-time assertions to ensure that invalid state machines cannot be compiled (for an idea what kinds of errors are reported at compile time, see the compile-fail tests). Above each of these assertions there is a comment explaining the problem. On almost all current compilers an error in template code is accompanied by the current "instantiation stack". Very much like the call stack you see in the debugger, this "instantiation stack" allows you to trace the error back through instantiations of library code until you hit the line of your code that causes the problem. As an example, here's the MSVC7.1 error message for the code in 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>> ]Depending on the IDE you use, it is possible that you need to switch to another window to see this full error message (e.g. for Visual Studio 2003, you need to switch to the Output window). Starting at the top and going down the list of instantiations you see that each of them is accompanied by a file name and a line number. Ignoring all files belonging to the library, we find the culprit close to the bottom in file InconsistentHistoryTest1.cpp on line 29.
Yes, see simple_state::clear_shallow_history() and simple_state::clear_deep_history(). Calling these functions is often preferable to introducting additional normal transitions when ...
Invisible to the user, the library uses static data members to implement
its own speed-optimized RTTI-mechanism for event<>
and
simple_state<>
subtypes. Whenever such a subtype is
defined in a header file and then included in multiple TUs, the linker
later needs to eliminate the duplicate definitions of static data members.
This usually works flawlessly as long as all these TUs are
statically linked into the same binary. It is a lot more complex
when DLLs are involved. The TuTest*.?pp files illustrate this:
Without any precautions (e.g. __declspec(dllexport)
on MSVC
compatible compilers), on most platforms both binaries (exe & dll) now
contain their own instance of the static data member. Since the RTTI
mechanism assumes that there is exactly one object of that member at
runtime, the mechanism fails spectacularly when the process running the exe
also loads the dll. Different platforms deal differently with this
problem:
__declspec(dllimport)
and __declspec(dllexport)
, which allow to define exactly
what needs to be loaded from a DLL (see TuTest for an example how to do
this). Therefore, the internal RTTI mechanism can be used but care must
be taken to correctly export and import all event<>
and simple_state<>
subtypes defined in headers that
are compiled into more than one binary. Alternatively, of course BOOST_STATECHART_USE_NATIVE_RTTI
can also be used to save the work of importing and exportingNo. 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 > { 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 > & ) { // ... } };
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<>
subtypes, 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, 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.
Revised 05 January, 2008
Copyright © 2003-2008 Andreas Huber Dönni
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)