Boost logo

Boost :

From: Andreas Huber (ah2003_at_[hidden])
Date: 2004-05-29 12:48:12


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


Boost list run by bdawes at acm.org, gregod at cs.rpi.edu, cpdaniel at pacbell.net, john at johnmaddock.co.uk