I have a state machine with one state which dispatches some message (e.g. text) to an external receiver. Before transiting to this state (let’s call it Dispatching) previous state needs somewhere to store that message so Dispatching can fetch it later. As message is created in one context and consumed in another, it will be created on the heap and State Manager object (which manages states, transitions and event loop) keeps a reference/pointer to it. State objects are created and destroyed as state machine transits through states. Each state inherits abstract base class State:
enum StateID
{
STATE_A,
STATE_B,
...
};
class State
{
public:
State(StateID stateID, StateManager& sm) :
stateID_(stateID), sm(sm_){}
virtual ~State(){};
virtual StateID HandleEvent(const Event& e) = 0;
StateID id() const {return stateID_;}
protected:
StateID stateID_;
StateManager& sm_;
};
In order to make passing data to the next state generic, I came up with the idea of StateData – a piece of information passed from one state to the next one. It is stored in dynamic memory, State Manager keeps a reference to it so each state can access it. As it is possible that different types of data will be passed to different states, StateData can be made abstract base class, specialized for each particular state:
struct StateData
{
virtual ~StateData() = 0;
};
struct StateAData : public StateData
{
int n_;
StateAData(int n) : n_(n){}
};
struct StateBData : public StateData
{
std::string str_;
StateBData(const std::string& str) : str_(str){}
};
...
class StateManager
{
boost::scoped_ptr<State> pCurrState_;
boost::scoped_ptr<StateData> pStateData_;
...
public:
void runEventLoop()
{
while(true)
{
...
//get event from a queue
...
StateID nextStateID = pCurrState_->HandleEvent(e);
if(nextStateID == pCurrState_->id())
continue;
pCurrState_.reset(0);
switch(nextStateID)
{
case STATE_A:
pCurrState_.reset(new StateA(*this));
break;
case STATE_B:
pCurrState_.reset(new StateB(*this));
break;
case STATE_C:
pCurrState_.reset(new StateC(*this));
break;
...
}
}
}
...
};
class StateA
{
public:
StateA(StateManager& sm) : State(STATE_A, sm){}
StateID HandleEvent(const Event& e)
{
switch(e.ID)
{
case EVENT_1:
{
StateAData* pData = reinterpret_cast<StateAData*>(stateMachine_.pStateData_.get());
// do something with data, e.g. use it for transition logic
if(pData->n_ % 2)
{
stateMachine_.pStateData_.reset(new StateBData("Hello from StateA"));
return STATE_B;
}
else
{
...
}
break;
}
...
}
}
...
}
There is a pitfall in following lines:
stateMachine_.pStateData_.reset(new StateBData("Hello from StateA"));
return STATE_B;
If transition logic changes so from here we need to go to STATE_C, developer can forget to change the type of StateBData to StateCData:
stateMachine_.pStateData_.reset(new StateBData("Hello from StateA"));
return STATE_C;
…which would lead to undesired behaviour when StateC tries to cast down StateData to StateCData.
How to avoid this? How to enforce matching the type of created object and returned enum value?
Yeah, this code stinks and this is the consequence of using two piece information and using enum to distinguish state types instead of types themselves. HandleEvent could return StateXData and depending on this returned type (as it carries the information about the next state) State Manager would determine (by using RTTI) the next state to transit to (X) but I don’t like this solution.
Another idea is to create an instance of the next state and pass its data into its constructor but this approach would pollute state machine design as one state would be created before previous state is destroyed.
Make the
enumpart of the base class or provide a purevirtualfunction in the base class to return thisenum. This way, the state itself will advertise its type.