
Robert Bell wrote:
Why else would you want to change the implementation (while leaving the interface as is)?
I don't know, I can't predict the future. Maybe you'll come up with an implementation strategy that you think is superior.
Is it true that the only reason you've ever had for changing an implementation is optimization? That's certainly not been true for me.
No, refactoring is the other reason but I'm very skeptical whether it is a good idea to force an IMHO more difficult interface onto users just to be able to *maybe* refactor the implementation in the future. This sounds to me like "Ummm, I'm not sure how to implement this, so lets design the interface in a way that makes implementation changes easier".
By using constructors and destructors for entry and exit, you're telling users that fsm guarantess that States will be constructed and destroyed at specific times, which means that for all time fsm must provide those guarantees.
Yes, what's so bad about that? We'd have to guarantee the same with entry() and exit().
It strikes me that making that guarantee with entry() and exit() may be easier to maintain if the implementation changes.
Only if I would see the slightest chance that such an implementation change will ever become necessary. To the contrary, entry() / exit() would inevitably complicate the implementation and I'm not convinced that it has any advantages whatsoever (performance or otherwise).
this essentially means to tell the users that they must not under any circumstances rely on when exactly constructors and destructors of state objects are called.
Keep in mind that it's entirely possible that I'm missign something. That said, they can't rely on when exactly constructors and destructors are called today.
Yes they can. It is clearly defined when state ctors/dtors are called.
I guess I was interpreting the word "exactly" a little differently. Saying "the constructor is called when the state is entered" means that it won't be called until the right sequence of events occurs, so I can't know "exactly" when the constructor will be called.
Yes, but the same reasoning applies to entry() / exit(), doesn't it? entry() / exit() doesn't buy you anything here.
Regardless of what was meant by "exactly" (it's clear I misinterpreted), I thought this was the more important point, so I'll ask again. Why would anyone care when and how many times the constructors and destructors are called?
That's exactly my point, if we had entry/exit, nobody *must* ever care. If you don't read the docs thoroughly and/or are tired, etc, you easily stick stuff in the ctor/dtor that doesn't belong there. That's why I think that entry()/exit() complicates the interface instead of simplifying it. If you only have ctor/dtor such errors are not possible, are they?
No, users are directly affected. Assume for a moment that we have entry()/exit () and that you want to create a non-pod object of type A in the entry action and destroy it in the exit action. For the reasons given before you cannot do this in the ctor/dtor. So, your state object cannot have a data member of type A, instead you need to either have A * (or the smart equivalent) as member. In entry(), you then allocate a new A object on the heap and set the data member pointer to that object. In exit() you need to delete the object and set the pointer back to 0.
I see what you're saying; I thought you meant something else by "explictly call constructors and destructors yourself".
Even if it's common to want to create a non-POD in an entry action and then destroy it in the corresponding exit action, I don't see a problem with doing the allocation in entry() and deallocation in exit(). But
You can easily forget one or the other. With ctor/dtor the compiler takes care of this for you. Why do you think have the C++ language designers arrived at the ctor/dtor design we have today? Exactly because they saw that manually calling ctor/dtor has led to problems in other languages.
I'm not even convinced that this usage is common anyway; do you have reason to think so?
Well, it is not super-common, but it does happen. Locks, custom arithmetic types and monostates come to mind.
constructor/destructor
-- demands that the state be created when it's entered and destroyed when it's exited -- no possibility for State member data to persist after exit unless the user somehow provides for it
Didn't we agree that the user couldn't rely on the lifetime of the state objects anyway (the argument was that I could more easily change the implementation and thus the lifetime of the state objects), or am I missing something?
-- only one state (plus parents) exists at a time; siblings cannot communicate with each other except through means such as global flags
With siblings I assume you mean inner states (e.g. in the StopWatch example Running and Stopped are siblings). I can't recall a single case where such siblings needed to communicate (I assume you mean access common variables). Maybe it's just because that for me it is *much* more natural to put such variables into a common outer state (e.g. see the Active::elapsedTime_ in the StopWatch example). The inner states share pretty much everything of their outer state anyway why not also share common variables?
entry()/exit()
-- only demands that a State be created once -- allows State data to persist after exit
This assumes that the state object is created before first entry and then lives until the state machine is destructed. This essentially throws away all implementation flexibility, which IIUC was one of the reasons to introduce entry()/exit().
I'm sure you've thought this out more than I have. I'm just coming from a gut-level reaction I have to linking entry/exit to construction/destruction. It just feels weird. For example, if entry and exit actions are things a state "does", then I'd expect them to be overridable; constructors and destructors can't be overridden.
How exactly would you want to override entry and exit actions? I mean, I personally haven't ever seen a framework allowing this, much less UML.
I'm not sure what your question means. If entry and exit actions are implemented as member functions called entry() and exit(), how else would you use them except by making them virtual and overriding?
I don't see how constructors and destructors are any different here, except for the fact that with entry()/exit() the base class implementation is not automatically called.
If I remember correctly, the reason this design evolved was because of a need to reuse entry/exit actions (although I don't remember exactly what the actions were). As I went forward, it made more and more sense to separate States and Actions. States represent the state the machine can be in; Actions do things. Having this separation made it easy to do things like combine Actions in interesting ways, such as defining a composite Action that executed a list of other Actions.
I have not felt the urge to do so myself, except for what boost::fsm does automatically anyway (i.e. the calling of multiple exit actions in the right order).
One example of a reuse of an entry Action was for debugging purposes. An Action that prints the ID of the State could be attached as an entry Action for each State to generate a trace of a machine's behavior.
Another reason for this design was that it occured to me that by having actions be member functions of State, it requires the user to derive new State classes to get different actions. One implication this had that I didn't like was that since the actual classes of the States in a machine would be dynamic, constructing a state machine was more complicated. I wanted to be able to do things like read a state machine description from an XML file and build it;
You'll never be able to do that with boost::fsm, unless you somehow manage to compile C++ on the fly. I think much of what you describe above and below may make sense in a dynamic environment (I don't have enough domain knowledge to judge). In such an environment you can shape pretty much everything of an FSM in an external file, *except* for the actions as they are still written in normal C++. I'm not that surprised that you need/want to combine actions in new ways as that can be done relatively easy in the external file without recompilation. The boost::fsm equivalent would be to do so with normal function calls.
I don't quite understand the objection to entry() and exit() member functions, but then again I don't have to understand it for fsm to proceed.
The objection is that it violates KISS. If there are hard *technical* arguments in favor of entry()/exit() then I'll happily add them. I hope we agree that so far no such arguments have been presented.
KISS is subjective; what's simple to one may not be simple to another.
That's true. However, I do hope that we agree that ctor/dtor *is* simpler from a strictly technical POV. While it may strike you as odd/more complex when you make your first steps, I'm quite confident that with time ctor/dtor is as simple as it gets. As I've explained above entry/exit does have issues a newbie might not be aware of. Regards, Andreas