开发者

State machine in C++ via singleton?

I think a good way of implementing a state machine is to use the singleton pattern. For example it can look like this:

class A
{

private:
    friend class State;
    State* _state;
    void change_state(State* state) { _state = state; }
};

class State
{开发者_如何学C
    virtual void action(A *a) = 0;
private:
    void change_state(A *a, State *state) { a->change_state(state); }
};

class StateA : public State
{
public:
    static State* get_instance()
    {
        static State *state = new StateA;
        return state;
    }
    virtual void action(A *a) { change_state(a, StateB::get_instance(); }
};

class StateB : public State
{
public:
    ...
    virtual void action(A *a) { change_state(a, StateA::get_instance(); }
};

My question is: I have read many articles about that the singleton pattern is so evil. Implementing this without a singleton pattern you have to call new everytime you change state, so for those who dont like singleton, how would you implement the state machine pattern?


I don't think that the singleton pattern is appropriate here. Singletons are good for representing abstract entities or physical objects for which there really is only one copy. To steal an example from Java, there is only one runtime environment in which a particular instance of the program executes. Singletons are good for representing these objects because they give the entire program the ability to name and reference it while preserving encapsulation and allowing for multiple possible backends.

Given this, I disagree that the singleton is the best route to take for your state machine. If you do implement it as a singleton, you're saying that is always exactly one copy of that state machine. But what if I want to have two state machines running in parallel? Or no state machines at all? What if I want my own local state machine so I can experiment on it to see what happens to it? If your state machine is a singleton, I can't do any of these things because there really is only one state machine used by the entire program.

Now, depending on how you're using the state machine, perhaps it is appropriate. If the state machine controls the overall execution of the program, then it might be a good idea. For example, if you're developing a video game and want a state machine to control whether you're in the menu, or in a chat area, or playing the game, then it would be totally fine to have a singleton state machine because there is only one logical state of the program at any time. From your question, though, I can't deduce if this is the case.

As for how to implement the state machine without a singleton, you might want to make the state machine object allocate its own copy of every state and build up the transition table (if you need explicit state objects), or just have a giant switch statement and a single enumerated value controlling what state you're in. If you have a single instance of the state machine this is no less efficient than the current version, and if you have multiple instances it allows you to store local information in each state without polluting a global copy of the states that could be read by other parts of the program.


Your StateA, StateB classes have no data members. Presumably other states won't have modifiable data members either, since if they did then that state would be weirdly shared between different instances of A, that is different state machines running concurrently.

So your singletons have avoided half of the problem with the pattern (global mutable state). In fact with only a small change to your design, you could replace the state classes with functions; replace the pointers to their instances with function pointers; and replace the virtual call to action with a call through the current function pointer. If someone gives you a lot of hassle for using singletons, but you're confident that your design is correct, you could make this minor change and see if they notice that their "correction" has made no significant difference at all to the design.

The other half of the problem with singletons still wouldn't be fixed though, and that's fixed dependencies. With your singletons, it is not possible to mock StateB in order to test StateA in isolation, or to introduce flexibility when you want to introduce a new state machine to your library which is the same as the current one except that StateA goes to StateC instead of StateB. You may or may not consider that a problem. If you do, then rather than making each state a singleton, you need to make things more configurable.

For example, you could give each state some identifier (a string or perhaps a member of an enum), and for each identifier register a State* somewhere in class A. Then rather than flipping to the singleton instance of StateB, StateA could could flip to whatever state object is used to represent "state B" in this state machine. That could then be a test mock for certain instances. You would still call new once per state per machine, but not once per state change.

In effect, this is still the strategy pattern for class A as in your design. But rather than having a single strategy to move the state machine forward, and continually replace that as the state changes, we have one strategy per state the machine passes through, all with the same interface. Another option in C++, that will work for some uses but not others, is to use (a form of) policy-based design instead of strategies. Then each state is handled by a class (provided as a template argument) rather than an object (set at runtime). The behavior of your state machine is therefore fixed at compile time (as in your current design), but can be configured by changing template arguments rather than by somehow altering or replacing the class StateB. Then you don't have to call new at all - create a single instance of each state in the state machine, as a data member, use a pointer to one of those to represent the current state, and make a virtual call on it as before. Policy-based design doesn't usually need virtual calls, because usually the separate policies are completely independent, whereas here they implement a common interface and we select between them at runtime.

All of this assumes that A knows about a finite set of states. This may not be realistic (for example, A might represent an all-purpose programmable state machine that should accept an arbitrary number of arbitrary states). In that case, you need a way to build up your states: first create an instance of StateA and an instance of StateB. Since each state has one exit path, each state object should have one data member which is a pointer to the new state. So having created the states, set the StateA instances "next state" to the instance of StateB and vice-versa. Finally, set A's current state data member to the instance of StateA, and start it running. Note that when you do this, you are creating a cyclic graph of dependencies, so to avoid memory leaks you might have to take special resource-handling measures beyond reference-counting.


In your code, you're not associating a state with the state machine the state belongs to (assuming that class A is the state machine). This information is passed in to the action method. So, if you had two instances of class A (i.e. two state machines) then you could end up having a state update the wrong state machine.

If you're doing this to avoid repeated calls to new and delete for speed purposes, then this is probably a premature optimisation. A better solution, if you can show that using new and delete is too slow / causes other issues (memory fragmentation for example), is to define an operator new / delete in the State base class that allocates from its own memory pool.

Here's some pseudocode for how the state machine I'm currently using works:

class StateMachine
{
public:
   SetState (State state) { next_state = state; }
   ProcessMessage (Message message)
   {
     current_state->ProcessMessage (message);
     if (next_state)
     {
       delete current_state;
       current_state = next_state;
       next_state = 0;
     }
   }
private:
   State current_state, next_state;
}

class State
{
public:
   State (StateMachine owner) { m_owner = owner; }
   virtual ProcessMessage (Message message) = 0;
   void *operator new (size_t size) // allocator
   {
     return memory from local memory pool
   }
   void operator delete (void *memory) // deallocator
   {
     put memory back into memory pool
   }
protected:
   StateMachine m_owner;
};

class StateA : State
{
public:
  StateA (StateMachine owner) : State (owner) {}
  ProcessMessage (Message message)
  {
    m_owner->SetState (new StateB (m_owner));
  }
}

The memory pool could be an array of chunks of memory, each big enough to hold any State, with a pair of lists, one for the allocated blocks and one for the unallocated blocks. Allocating a block then becomes a process of removing a block from the unallocated list and adding it to the allocated list. Freeing is then the reverse process. I think the term 'free list' for this type of allocation strategy. It is very fast but has some wasted memory.


One approach which assumes that all state objects live along StateMachine could be like this one:

enum StateID
{
   STATE_A,
   STATE_B,
   ...
};

// state changes are triggered by events 
enum EventID
{
   EVENT_1,
   EVENT_2,
   ...
};

// state manager (state machine)
class StateMachine
{
   friend StateA;
   friend StateB;
   ...

public: 
   StateMachine();
   ~StateMachine();
   // state machine receives events from external environment
   void Action(EventID eventID);
private:
   // current state
   State* m_pState;

   // all states
   StateA* m_pStateA;
   StateB* m_pStateB;
   ... 

   void SetState(StateID stateID);       
};

StateMachine::StateMachine()
{
   // create all states
   m_pStateA = new StateA(this, STATE_A);
   m_pStateB = new StateB(this, STATE_B);
   ...

   // set initial state
   m_pState = m_pStateA; 
}

StateMachine::~StateMachine()
{
   delete m_pStateA;
   delete m_pStateB;
   ...
}

void StateMachine::SetState(StateID stateID)
{
   switch(stateID)
   {
   case STATE_A:
      m_pState = m_pStateA;
      break;
   case STATE_B:
      m_pState = m_pStateA;
      break;
   ...
   }
}

void StateMachine::Action(EventID eventID)
{
   // received event is dispatched to current state for processing
   m_pState->Action(eventID);
}

// abstract class
class State
{
public:
   State(StateMachine* pStateMachine, StateID stateID);
   virtual ~State();
   virtual void Action(EventID eventID) = 0;
private:
   StateMachine* m_pStateMachine;
   StateID m_stateID;       
};

class StateA : public State
{
public: 
   StateA(StateMachine* pStateMachine, StateID stateID);    
   void Action(EventID eventID);
};

StateA::StateA(StateMachine* pStateMachine, StateID stateID) : 
   State(pStateMachine, stateID) {...}

void StateA::Action(EventID eventID)
{
   switch(eventID)
   {
   case EVENT_1:
      m_pStateMachine->SetState(STATE_B);
      break;
   case EVENT_2:
      m_pStateMachine->SetState(STATE_C);
      break;
   ...
   }
}

void StateB::Action(EventID eventID)
{
   switch(eventID)
   {
   ...
   case EVENT_2:
      m_pStateMachine->SetState(STATE_A);
      break;
   ...
   }
}

int main()
{
   StateMachine sm;
   // state machine is now in STATE_A

   sm.Action(EVENT_1);
   // state machine is now in STATE_B

   sm.Action(EVENT_2);
   // state machine is now in STATE_A

   return 0;
}

In more complex solution StateMachine would have event queue and event loop which would wait for events from the queue and dispatch them to the current state. All time-consuming operations in StateX::Action(...) should run in separate (worker) thread in order to prevent blocking event loop.


A design approach I am considering is to create a state factory that is a singleton, so that more then one state machine can use the state objects produced by the factory.

But this thought has taken me to the idea of implementing my state factory with flyweight pattern and that's where I have stopped.

Basically, I need to research the advantages of implementing the state objects as flyweights and then the advantages of a flyweight design pattern.

I have heard of this state machines using this type of pattern, but not sure if it will work for my needs.

Anyway, I was doing some research and bumped into this post. Just thought I would share...

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜