I'm looking for some general
- Optimization
- Correctness
- Extensibility
advice on my current C++ Hierarchical State Machine implementation.
Sample
variable isMicOn = false
variable areSpeakersOn = false
variable stream = false
state recording
{
//override block for state recording
isMicOn = true //here, only isMicOn is true
//end override block for state recording
}
state playback
{
//override block for state playback
areSpeakersOn = true //here, only areSpeakersOn = true
//end override block for state playback
state alsoStreamToRemoteIp
{
//override block for state alsoStreamToRemoteIp
stream = true //here, both areSpeakersOn = true and stream = true
//end override block for state alsoStreamToRemoteIp
}
}
goToState(recording)
goToState(playback)
goToState(playback.alsoStreamToRemoteIp)
Implementation
Currently, the HSM is implemented as a tree structure where each state can have a variable number of states as children.
Each state contains a variable number of "override" blocks (in a std::map) that override base values. At the root state, the state machine has a set of variables (functions, properties...) initialized to some default values. Each time we enter a child state, a list of "overrides" define variable and values that should replace the variables and values of the same name in the parent state. Updated original for clarity.
Referencing variables
At runtime, the current states are stored on a stack.
Every time a variable is referenced, a downwards stack walk is performed looking for the highest override, or in the case of no overrides, the default value.
Switching states
Each time a single state frame is switched to, the state is pushed onto a stack.
Each time a state is switched to, I trace a tree descension that takes me from the current state to the root state. Then I do a tree descension from the target state to the root state until I see the current trace matches the previous trace. I declare an intersection at where those 2 traces meet. Then, to switch to the target state, I descend from the source, popping state frames from the stack until I reach the intersection point. Then I ascend to the target node and push state frames onto the stack.
So for the code sample above
Execution trace for state switch
- Source state = recording
Target State = alsoStreamToRemoteIp
descension from source = recording->root (trace = [root])
descension from target = alsoStreamToRemoteIp->playback->root (trace = [playback, root])
Intersects at root.
To switch from recording to alsoStreamToRemoteIp,
- Pop "recording" from the stack (and call its exit function... not defined here).
- Push "playback" onto the stack (and call the enter function).
- Push "alsoStreamToRemoteIp" onto the stack (and call the the enter function).