C++ 中的状态机通过单例?

发布于 2024-10-16 18:36:50 字数 770 浏览 3 评论 0原文

我认为实现状态机的一个好方法是使用单例模式。 例如,它可以看起来像这样:

class A
{

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

class State
{
    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(); }
};

我的问题是:我读过很多关于单例模式是如此邪恶的文章。在没有单例模式的情况下实现这一点,每次更改状态时都必须调用 new,因此对于那些不喜欢单例的人,您将如何实现状态机模式?

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
{
    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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

极致的悲 2024-10-23 18:36:51

我认为单例模式在这里不合适。单例适用于表示实际上只有一个副本的抽象实体或物理对象。借用 Java 的一个例子,只有一个运行时环境可以执行程序的特定实例。单例非常适合表示这些对象,因为它们使整个程序能够命名和引用它,同时保留封装并允许多个可能的后端。

鉴于此,我不同意单例是状态机的最佳途径。如果您确实将其实现为单例,那么您就是说它始终是该状态机的一个副本。但是如果我想让两个状态机并行运行怎么办?或者根本没有状态机?如果我想要自己的本地状态机,以便我可以对其进行实验,看看会发生什么情况,该怎么办?如果你的状态机是单例的,我就不能做任何这些事情,因为整个程序实际上只有一个状态机。

现在,根据您如何使用状态机,也许它是合适的。如果状态机控制程序的整体执行,那么这可能是一个好主意。例如,如果您正在开发视频游戏并希望状态机来控制您是在菜单中,还是在聊天区域中,或者在玩游戏,那么拥有一个单例状态机就完全没问题,因为程序在任何时候都只有一种逻辑状态。不过,从你的问题来看,我无法推断情况是否如此。

至于如何在没有单例的情况下实现状态机,您可能想让状态机对象分配每个状态的自己的副本并构建转换表(如果您需要显式状态对象),或者只是有一个巨大的 switch 语句和一个控制您所处状态的单个枚举值。如果您有状态机的单个实例,这并不比当前版本的效率低,如果您有多个实例,它允许您存储每个状态中的本地信息,而无需污染可以被程序其他部分读取的状态的全局副本。

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.

哎呦我呸! 2024-10-23 18:36:51

您的 StateAStateB 类没有数据成员。想必其他状态也不会具有可修改的数据成员,因为如果它们这样做,那么该状态将在 A 的不同实例之间奇怪地共享,即同时运行的不同状态机。

所以你的单例已经避免了该模式的一半问题(全局可变状态)。事实上,只需对设计进行很小的更改,您就可以用函数替换状态类;用函数指针替换指向其实例的指针;并将对 action 的虚拟调用替换为通过当前函数指针的调用。如果有人给你使用单例带来了很多麻烦,但你确信你的设计是正确的,你可以做这个小小的改变,看看他们是否注意到他们的“修正”对设计没有任何显着的影响。

不过,单例问题的另一半仍然没有得到解决,那就是固定的依赖关系。使用单例,不可能模拟 StateB 来单独测试 StateA,或者当您想向库中引入一个新的状态机时引入灵活性,该状态机与当前状态机相同,只是 StateA 转到 StateC B 国。您可能认为这是一个问题,也可能不认为这是一个问题。如果您这样做,那么您需要使事情变得更加可配置,而不是使每个状态成为单例。

例如,您可以为每个状态提供一些标识符(字符串或枚举的成员),并为每个标识符在类 A 中的某处注册一个 State* 。然后,而不是翻转到单例实例对于 StateB,StateA 可以翻转到用于表示此状态机中的“状态 B”的任何状态对象。这可能是某些情况下的测试模拟。您仍然会在每台机器的每个状态调用一次 new ,但不会在每次状态更改时调用一次。

实际上,这仍然是您设计中 A 类的策略模式。但是,我们并不是采用单一策略来推动状态机前进,并随着状态的变化不断替换它,而是为机器经过的每个状态制定一个策略,所有策略都具有相同的接口。 C++ 中的另一个选项(适用于某些用途,但不适用于其他用途)是使用基于策略的设计(一种形式)而不是策略。然后每个状态由一个类(作为模板参数提供)而不是一个对象(在运行时设置)处理。因此,状态机的行为在编译时是固定的(如在当前设计中),但可以通过更改模板参数来配置,而不是通过某种方式更改或替换类 StateB。然后,您根本不必调用 new - 创建状态机中每个状态的单个实例,作为数据成员,使用指向其中一个状态的指针来表示当前状态,并且像以前一样对其进行虚拟调用。基于策略的设计通常不需要虚拟调用,因为通常单独的策略是完全独立的,而这里它们实现了一个公共接口,我们在运行时在它们之间进行选择。

所有这些都假设 A 知道一组有限的状态。这可能不现实(例如,A 可能代表一个通用可编程状态机,它应该接受任意数量的任意状态)。在这种情况下,您需要一种方法来构建状态:首先创建 StateA 的实例和 StateB 的实例。由于每个状态都有一个退出路径,因此每个状态对象应该有一个数据成员,它是指向新状态的指针。因此,创建状态后,将 StateA 实例的“下一个状态”设置为 StateB 的实例,反之亦然。最后,将A的当前状态数据成员设置为StateA的实例,并启动它运行。请注意,执行此操作时,您正在创建依赖关系的循环图,因此为了避免内存泄漏,您可能必须采取引用计数之外的特殊资源处理措施。

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.

苏璃陌 2024-10-23 18:36:51

在您的代码中,您没有将状态与该状态所属的状态机关联起来(假设 A 类是状态机)。该信息被传递到操作方法。因此,如果您有两个 A 类实例(即两个状态机),那么您最终可能会更新错误的状态机。

如果您这样做是为了避免出于速度目的而重复调用 new 和 delete,那么这可能是一个过早的优化。如果您可以证明使用 new 和 delete 太慢/导致其他问题(例如内存碎片),更好的解决方案是在 State 基类中定义一个从其自己的内存池分配的运算符 new/delete。

以下是我当前使用的状态机如何工作的一些伪代码:

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));
  }
}

内存池可以是内存块的数组,每个内存块都足够大以容纳任何状态,并带有一对列表,一个用于分配的块,一个用于已分配的块。未分配的块。分配块就变成了从未分配列表中删除块并将其添加到已分配列表的过程。释放则是相反的过程。我认为术语“空闲列表”适合这种类型的分配策略。它非常快,但会浪费一些内存。

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.

作业与我同在 2024-10-23 18:36:51

假设所有状态对象都存在于 StateMachine 中的一种方法可能如下:

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;
}

在更复杂的解决方案中,StateMachine 将具有事件队列和事件循环,它们将等待队列中的事件并将它们分派到当前状态。 StateX::Action(...) 中的所有耗时操作都应在单独的(工作)线程中运行,以防止阻塞事件循环。

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.

随风而去 2024-10-23 18:36:51

我正在考虑的一种设计方法是创建一个单例状态工厂,以便多个状态机可以使用工厂生成的状态对象。

但这个想法让我产生了用蝇量模式实现我的状态工厂的想法,这就是我停止的地方。

基本上,我需要研究将状态对象实现为享元的优点,然后研究享元设计模式的优点。

我听说过这种状态机使用这种类型的模式,但不确定它是否适合我的需要。

无论如何,我正在做一些研究并偶然发现了这篇文章。只是想我会分享...

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...

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文