Re: Factories, handles, and handle wrappers
On Dec 17, 7:42 pm, Maxim Yegorushkin <maxim.yegorush...@gmail.com>
wrote:
On 17/12/09 18:19, James Kanze wrote:
On Dec 16, 9:50 pm, Maxim Yegorushkin<maxim.yegorush...@gmail.com>
wrote:
On 16/12/09 13:50, Michael Mol wrote:
> Let's say you have a factory which returns handles to objects
> that it allocates. You pass these handles to the factory
> whenever you want to use the object or destroy it. The
> factory's "OperateOnObject" class verifies that the handle is
> within its object collection, and then marshals the
> OperateOnObject call to the class's internal OperateOnObject
> method. All external operations on the object instance are
> required to pass through the Factory in order to guarantee
> that the object instance is still valid at the time of call.
> This leads to code roughly like this. (Not using exceptions
> here, but rather return error codes.)
> // For the sake of scope, consider this as a global singleton factory.
> // For the sake of concerns over initialization and destruction,
> assume that's dealt with in code not shown.
> class SomeFactory
> {
> public:
> OBJHANDLE CreateObject();
> ERRCODE DestroyObject(OBJHANDLE);
> ERRCODE OperateOnObject(OBJHANDLE objHandle, int someArgument);
> protected:
> OBJCOLLECTION objCollection;
> } factoryObj;
> // In some function, somewhere
> OBJHANDLE objHandle = factoryObj.CreateObject();
> factoryObj.OperateOnObject(objHandle, 42);
> factoryObj.DestroyObject(objHandle);
In other words, you've got:
1) A factory that creates objects.
2) Those objects implement an interface, which is currently belongs to
factory class.
3) You'd also like for the factory to check whether the object reference
is valid.
You can refactor this to simply things.
1) Extract object interface from the factory.
struct SomeObject {
// former SomeFactory::OperateOnObject
virtual ERRCODE Operate(int someArgument) = 0;
virtual ~SomeObject() = 0;
};
Using such object now does not require a factory object, i.e. you
can call Operare() directly on the object.
2) Make factory return smart-pointers to SomeObject. The objects it
creates implement SomeObject interface.
typedef boost::shared_ptr<SomeObject> SomeObjectPtr;
class SomeFactory {
public:
SomeObjectPtr createSomeObject();
...
};
Now the factory function returns a smart-pointer. This
smart-pointer takes care of destroying the object when it is no
longer used. No manual object destruction required.
Except that boost::smart_ptr won't necessarily work here---it will
render destruction non-deterministic,
I would say that the destruction is perfectly deterministic -- when the
last copy of the shared_ptr<> referencing that particular object is
destroyed.
In other words, undeterministic, at least in practice. Possibly
never,
and almost certainly later than required.
(Although, in practice, I normally prefer factories to return
auto_ptr<> to emphasize the fact that the ownership of a newly created
object is being transferred).
I too find that auto_ptr is a good choice when creating objects,
especially when additional actions are needed until they're capable of
taking care of themselves. Sometimes between the creation and the end
of the transaction, of course, release will be called on the auto_ptr,
so that it will no longer delete anything, but until the object is
fully
registered and installed in the runtime system, auto_ptr provides a
nice
(essential?) safety net.
> and will cause objects to "leak" as soon as there are any cycles.
It is interesting that you mention non-deterministic destruction and
leaks due to cycles with regards to one subject.
Normal, since they're the two major problems with shared_ptr. (The
other major problem with boost::shared_ptr is that it's too easy to
end
up with two separate counters. This can be easily cured by using
invasive shared pointers, however.)
To me it looks like leaks due to cycles are an inherent problem of
precisely deterministic destruction, when some form of reference
counting is used.
Leaks due to cycles are an inherent problem of reference counting,
yes.
But they have nothing to do with destruction, per se.
For example, Perl and Python use deterministic destruction and in
these languages leaks are possible due to cycles. Java, on the other
hand, offers non-deterministic destruction which does not suffer from
cycles (but suffers from non-determinism, which to me is a bigger evil
than cycles).
Java doesn't offer "destruction" at all---deterministic or
otherwise---at the language level. Nothing in Java, however, forbids
defining a function (traditionally named "dispose") which "destructs"
the object. And is called at a deterministic time.
Of course, this is also possible in C++ (with some sort of garbage
collection for recovering memory), but the name (and the tradition)
says
that it is the destructor which destructs or disposes of the
object---which terminates its lifetime.
Of course, his solution won't work either, since without garbage
collection, there's absolutely no way to ensure that the invalid
pointer remains invalid.
3) Using a smart-pointer makes the possibility of using an already
destroyed object highly unlikely.
True. But it does so by not destroying the objects when they should
be destroyed, and possibly never. The cure is as bad as the
disease.
Feeling a bit thick, could you possibly elaborate the last point?
Objects have defined, deterministic lifetimes. In some cases, they
can
(or should) continue to exist even when there is no visible pointer to
them. (To be useful, there must be a pointer to them somewhere, but
this can be a pointer hidden somewhere in the OS, which causes them to
receive external events.) And in many cases, they need to be
destructed
even if there are pointers to them. The logic is the opposite of
smart
pointers or garbage collection: you don't destruct the object or free
the memory because there are no more pointers to it; you eliminate all
pointers to it because it's lifetime has logically ended. And in most
applications, the relationships between objects (represented by
pointers) do not form a tree or a DAG. There will be cycles. Which
means that if you're counting uniquely on shared_ptr, destructors
won't
be called when they should be, and in many cases, they won't be called
at all, because of the cycles.
Of course, with boost::shared_ptr, you can use boost::weak_ptr to
break
the cycles. But this requires practically the same amount of work as
doing all of the management by hand, and it still doesn't solve the
problem of determinism: an external event determines that the object's
lifetime should end, not whether or not there is still a pointer to
it.
--
James Kanze