Mick Hollins wrote:
When I implemented history, I had to make a choice between:
a) The current behavior b) Abandon the entry-action --> ctor / exit-action --> dtor mappings and introduce enter() and exit() functions instead (exit() already exists for unrelated reasons). In non-history transitions the state objects would be constructed and destructed as they are now (and enter() / exit() would be called after construction / before destruction). However, when a "historized" state is exited, exit() is called but the state is not destructed. Instead, it is stored in the state_machine object. When a transition to history is made later the state is retrieved and only enter() is called.
Given my recent experience, (b) sounds good to me :-)
Ok, noted.
Would it be feasible to support both models somehow?
It might be, by letting users that require b) derive from a special state.
In my experience none of the above behaviors is entirely satisfying in practice because for some states you want a) and for others you'd want b).
Well, for states not involved in history transitions, (a) and (b) are more or less equivalent in terms of power of expression. I guess (a) is a little nicer as it doesn't involve 2-phase construction.
Exactly.
For states that are involved in history transitions, it seems to me that (b) is the winner since it distinguishes between the two ways in which a state can be entered and is thereby more flexible.
It would definitely be more flexible, no question about that. See below.
Om the subject of two phase construction, I often find that you need it for states anyway, as what you often want is the ability to provide parameters to the constructor of a state to which you are transitioning.
Yes, that is a common problem. AFAICT, UML only allows for event access in a transition action but not in entry and exit actions.
The approach I've settled on for this is to post an "initialisation event" just before transitioning state, so that the initialisation event will be the first event handled by the state to which I am transitioning. Is there a better way to achieve this?
Not currently. triggering_event(), which is on position 2 of the to-do list, would allow ctors, dtors & exit() functions to access the event that triggered the state entry or exit. Since these can be triggered by events of different types, the function only returns the base class event, which needs to be down-cast by the user. I'm not very happy with this interface though, so I will also consider the possibility to type-safely pass the triggering event to state constructors that accept a corresponding parameter. AFAICS, this would require a SFINAE compiler (i.e. a very compliant compiler). Unfortunately, due to inevitable type-erasure when the state is stored in state_machine, the same doesn't seem to be possible with exit() functions, at least not without major performance hits.
When a state contains two members you could even want a) for the first one and b) for the second one.
(b) allows you to have two members with different frequency of initialisation, but (a) makes that harder as it doesn't distinguish between time of state construction and time of (re)entry to a state.
With b) you'd need to do extra work to initialize non-POD variables you want reset for normal and history entry.
Moreover, you can still get the desired behavior with a), by pushing the state-variables that you want preserved into outer states or the state machine object. As you observed, this essentially wrecks the benefits of state-local storage for those variables. I guess the main reasons that I settled with a) are: 1. It is simpler to explain and implement. 2. There were no votes in favor of b). In fact, you are the first user to bring this up.
Are you saying that I am the first person to attempt to use state local storage and history together?
No. I know of at least 2 users that use both history and state-local storage. They were/are apparently happy with the current behavior.
3. History is used relatively rarely anyway.
I love the history facility (of both UML statecharts and boost::statechart). In fact, history ranks in my top 3 things I love about UML statecharts. The top 3 being:
1) heirarchical states 2) history states 3) deferred event delivery
Ok, noted. My guess is that only about 1/3 of the users really need history. No hard numbers, just a feeling.
For the record, what I love about boost::statechart is:
1) It makes it trivial to map from a UML statechart to C++ code. When programming using boost::statechart I spend a great deal of my time working at the diagram level, rather than at the code level. That is great for productivity. I've never really experienced such a degree of productivity improvement from other bits of UML. 2) State local storage lets me constrain the scope of variables to exactly where I need them
I find state local storage so nice that I like to use it for all my states. The problem is that if I realise down the track that a state needs to support history transitions then I need to rewrite the state to avoid using state local storage, possibly only after having tracked down a nasty bug caused by the history transitions.
I was aware of the theoretical problem but I didn't realize that it could be so relevant in practice. When realizing that a state needs to support history transitions, how many of the state-local member do you usually need to push outward? All of them or just a fraction? Also, I would appreciate your sharing of some numbers of your use case: - Number of machines - Number of states - Number of states with history (shallow/deep) - Number of history transitions + number of history initial states
Then again, almost all my experience with state machines stems from the machine control field. Other areas might well have different requirements / expectations, so I'm interested in your use-case and how much of a difference b) would make. Also, there might be other, less intrusive approaches that I haven't thought about.
I've started to think about other ways to represent state local storage that work well with history. If you have any suggestions, I'd love to hear them.
Library support similar to b) would require a full rewrite of the history facility and would almost certainly break existing code at runtime (code that relies on member variables being initialized in the state ctor, no matter whether the state is entered through history or normally). Moreover, the interface would become more complex (users not yet using history would rightly ask themselves where to put their entry action and I wouldn't be able to give a clear guideline). I think it's obvious that I'd need more people requesting b) before I could justify the effort and the negative consequences. Plus, even when there are enough people the chance of having a working implementation in the next 6 months are very slim (I'm currently a bit short in spare time :-(). In the mean time, I think one could alleviate the problem a little bit: // pseudo-code template< /* same parameters as state_machine */ > struct micks_state_machine : sc::state_machine< /* forward parameters */
{ template< typename T > void store_persistent_member( /* .. */ ); template< typename T > T retrieve_persistent_member( /* ... */ ); bool has_persistent_member( /* ... */ ); private: // hash_map that maps an ID to boost::any }; // Use this class instead of normal members in states that are // entered through histroy transitions. template< class Machine, class T > class persistent_member { public: persistent_member( Machine & m, T initial ) { if ( m.has_persistent_member( /* ... */ ) { member = m.retrieve_persistent_member( /* ... */ ); } else { member = initial; } } ~persistent_member() { m.store_persistent_member( /* ... */ ); } T & get() { return member; } private: Machine & m; T member; } I realize this only works with states that are entered through history only, because you currently have no way of knowing whether you have normal or history entry... HTH & Regards, -- Andreas Huber When replying by private email, please remove the words spam and trap from the address shown in the header.