C++Talk.NET Forum Index C++Talk.NET
C++ language newsgroups
 
Archives   FAQFAQ   SearchSearch   MemberlistMemberlist   UsergroupsUsergroups   RegisterRegister 
 ProfileProfile   Log in to check your private messagesLog in to check your private messages   Log inLog in 

Problem with design of a class: ownership of data, copy cons

 
Post new topic   Reply to topic    C++Talk.NET Forum Index -> C++ Language (Moderated)
View previous topic :: View next topic  
Author Message
Martin
Guest





PostPosted: Tue Sep 27, 2005 2:52 pm    Post subject: Problem with design of a class: ownership of data, copy cons Reply with quote




I am learning C++, and slowly trying to improve my style... I am now
confronted to a design decision that looks like a choice between
Charybde and Scylla, probably because of past bad decisions I made. If
you could help me out of this of state of perplexity, I would be very
grateful.


I want a class (or rather a hierarchy of classes) to implement the
different operations which can be performed on a file. One of its uses
would be inside undo/redo stacks - for an editor. If you think this
design is stupid from the start, please say so Smile.
So my starting point would be a base abstract class Operation. Each
specific Operation ( Delete, Paste, edition of a field... ) would be
implemented in a derived class.

I also need a way to batch the elementary operations into more complex
ones. The undo/redo stacks will have a "batching state" controlled
through BeginBatch() EndBatch() calls, but I am very hesitant as to
the design of these batches at the level of my Operation class.


I could create a BatchOfOperations class derived from Operation to
implement those (since batches are operations themselves and must be
stored in the undo/redo stack):

First option I see is simply to pass pointer to Operation objects to
the batch, and store them. I have some doubts about the ownership
policy, and how to maintain it though. In what follows the batch takes
ownership of the operation objects. The semantics is not very natural
to say the least, since I end up manipulating pointer to Operation
objects where Operation objects would make more sense.



class Operation
{
public:
virtual ~Operation(){};

/* performs the operation on Basefile& file,
Might invalidate the Operation object when called (make it an
empty operation):
In case of a paste operation for instance, ownership of the
data stored in the operation might actually have been
transferred to the file for the sake of optimization, and so
the operation should not be able to touch it anymore.
*/
virtual operator()(BaseFile & file) = 0;

protected:
virtual void AddTo(BatchOfOperations* batch)
{
batch->Push(this);

}
private:
//non copyable because the semantics don't fit.
Operation(Operation&);
Operation& operator=(Operation&);
};
class BatchOfOperations : public Operation
{

/* each BatchOfOperations stores pointers to the Operations it
groups, it is responsible for deleting them in the destructor,
or possibly when operator() is called */

explicit BatchOfOperations(Operation*& op)
{
Add(op);
}

/* adding Operation *op to our batch
the batch takes ownership of the Operation object op:
*/
void Add(Operation*& op)
{
op->AddTo(this);
op = 0;// make sure this pointer won't be used again
}

protected:
void AddTo(BatchOfOperations& batch)
{
batch.Combine(this);
}
private:

/* adds op to the pointers we store
*/
void Push(Operation* op);


/* retrieves the pointers to Operation objects stored in op
and add them to the ones we store.
Considering our ownership policy should at least empty op of
all its pointers.
This is one of the places where I am uncomfortable with the
ownership policy: Should I assume that op has been dynamically
allocated and simply delete it. This seem in accordance with
the semantics, and furthermore safe: Since void
Add(Operation*& op) takes a non const reference to a pointer,
it guaranties that the object pointed to by op cannot have
automatic lifespan, right? Or I am utterly lost and making that
up?
*/
void Combine(BatchOperation* op);

};


All in all I find the semantics fragile at first view and somewhat
unnatural, can something like that be made dependable and acceptable?
It seems



A variant would be to clone every Operations I want to store in the
batch, the result is much better semantically I think, but I am going
to have problems behind the scene.
The data needed to perform certain operation is extensive, a paste
operation for instance will have to store the piece of the file to
paste (and the files the editor is manipulating are relatively big,
typically 10 or 20 Mo of data). If I want to maintain the integrity of
every copy of an Operation object, I'll have to either duplicate that
data (not really an option in this context) or keep a reference count
on it. The same is true when calling operator()(Basefile&) instead of
simply transferring ownership of the data, I will have to use a
reference count, that will necessitate a big rewriting of associated
classes... In the end I wonder if I will not make things more
complicated than they should be (especially considering I am almost
sure there are some problems I am not seeing yet) for a limited
benefit.

class Operation
{
public:
virtual ~Operation(){};
virtual operator()(BaseFile &) = 0; // performs the operation
on Basefile
protected:
virtual void AddTo(BatchOfOperations* batch)
{
batch->Push(this);
}
virtual Operation* Clone() = 0;
};

class BatchOfOperations : public Operation
{

/* each BatchOfOperations can safely delete the Operation */

explicit BatchOfOperations(Operation*& op)
{
Add(op);
}

/* adding Operation op to our batch*/
BatchOfOperations& Add(const Operation& op)
{
Operation* clone = op.Clone();
clone->AddTo(this);
}

protected:
void AddTo(BatchOfOperations& batch)
{
batch.Combine(this);
}
private:

/* adds op to the pointers we store
*/
void Push(Operation* op);

/* retrieves the pointers to Operation objects stored in op
and add them to the ones we store.
Considering our ownership policy should empty op of all its
pointers (make it an empty batch) */
void Combine(BatchOperation* op);

};


An other approach would be to discard the BatchOfOperations class
altogether, and instead make every operation a potential "starting
point" for a batch something like:

class Operation
{
operator()(Basefile& file)
{
ApplyTo(file);
if(nextOp_)
{
(*nextOp_)(file);
}
}
private:
Operation* nextOp_;
/* implements the actual operation: */
virtual DoApply(BaseFile*) = 0;
};

But this doesn't really change a thing, I end up with the same
problems and alternative as with the first two options (in terms of
ownership of the operations linked to, data, copy construction...).

Thanks for your advices.

[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]

Back to top
Patrick May
Guest





PostPosted: Wed Sep 28, 2005 9:47 am    Post subject: Re: Problem with design of a class: ownership of data, copy Reply with quote



Martin <martin (AT) proctophantasmist (DOT) cjb.net> writes:
Quote:
I want a class (or rather a hierarchy of classes) to implement the
different operations which can be performed on a file. One of its
uses would be inside undo/redo stacks - for an editor.

You should look into the Command pattern (Google returns several
million hits if you don't own "Design Patterns").

Regards,

Patrick

------------------------------------------------------------------------
S P Engineering, Inc. | The experts in large scale distributed OO
Quote:
systems design and implementation.
[email]pjm (AT) spe (DOT) com[/email] | (C++, Java, Common Lisp, Jini, CORBA, UML)


[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]


Back to top
kanze
Guest





PostPosted: Wed Sep 28, 2005 11:03 am    Post subject: Re: Problem with design of a class: ownership of data, copy Reply with quote



Martin wrote:

Quote:
I want a class (or rather a hierarchy of classes) to implement
the different operations which can be performed on a file. One
of its uses would be inside undo/redo stacks - for an editor.
If you think this design is stupid from the start, please say
so Smile. So my starting point would be a base abstract class
Operation. Each specific Operation ( Delete, Paste, edition of
a field... ) would be implemented in a derived class.

I also need a way to batch the elementary operations into more
complex ones. The undo/redo stacks will have a "batching
state" controlled through BeginBatch() EndBatch() calls, but I
am very hesitant as to the design of these batches at the
level of my Operation class.

Isn't a Batch just a collection of Operation? And an Operation
itself?

(You don't say what you really mean by batch here. I'm
interpreting it as a group of operations which must be executed
as one; although that's not really the usual meaning, it's the
only one which seems to fit with the idea of using it in a
redo/undo stack.)

Quote:
I could create a BatchOfOperations class derived from
Operation to implement those (since batches are operations
themselves and must be stored in the undo/redo stack):

Exactly. That sounds exactly what is needed.

Quote:
First option I see is simply to pass pointer to Operation
objects to the batch, and store them. I have some doubts about
the ownership policy, and how to maintain it though.

The obvious question is: why do you need an ownership policy.
Typically, these kind of operations have trivial destructors.
So just use garbage collection, and forget about them; all other
solutions are just excess work for nothing.

If for some reason you cannot use garbage collection, I can't
imagine cycles in the Operation, so boost::shared_ptr could
probably be used. Or an in-house invasive shared pointer, if
for some reason you cannot use Boost. (In the past, I regularly
made these types of objects derive from RefPtrObj, using a
reference pointer implementation very much like the one in Scott
Meyer's books. In this case, it's a simple and -- if the
application is mono-thread -- safe solution.)

Quote:
In what follows the batch takes ownership of the operation
objects. The semantics is not very natural to say the least,

Agreed. There are cases where a transfer of ownership is fully
natural; that's what std::auto_ptr is for. But in this case, I
would imagine that shared ownership, or even no ownership, is
more natural.

Quote:
since I end up manipulating pointer to Operation objects where
Operation objects would make more sense.

class Operation
{
public:
virtual ~Operation(){};

/* performs the operation on Basefile& file,
Might invalidate the Operation object when called (make it an
empty operation):
In case of a paste operation for instance, ownership of the
data stored in the operation might actually have been
transferred to the file for the sake of optimization, and so
the operation should not be able to touch it anymore.
*/
virtual operator()(BaseFile & file) = 0;

protected:
virtual void AddTo(BatchOfOperations* batch)
{
batch->Push(this);

}

This looks backwards. Operation shouldn't know whether there is
derived class supporting batching or not. This functionality
belongs strickly in the Batch class.

Quote:
private:
//non copyable because the semantics don't fit.

Or more strictly speaking, because copy/assignment and
inheritance don't mix well. There's nothing fundamentally wrong
with copying an agent, but it's difficult to make it work
correctly in C++ if the polymorphism is dynamic (the STL does it
all the time -- but the resolution is at compile time, not at
run time), and generally not worth the effort.

Quote:
Operation(Operation&);
Operation& operator=(Operation&);
};
class BatchOfOperations : public Operation
{
/* each BatchOfOperations stores pointers to the
Operations it groups, it is responsible for deleting
them in the destructor, or possibly when operator()
is called */

explicit BatchOfOperations(Operation*& op)
{
Add(op);
}

Why no default constructor? It's absense means that adding the
initial operation is a special case. Sometimes, it naturally
will be anyway, but it's probably not a good idea to impose it.

Quote:
/* adding Operation *op to our batch
the batch takes ownership of the Operation object op:
*/
void Add(Operation*& op)
{
op->AddTo(this);
op = 0;// make sure this pointer won't be used again
}

If this is really what you want to do, then the signature should
be:

void Add( std::auto_ptr< Operation > op )

Also, I don't see the reason for the indirection through
Operation.

Quote:
protected:
void AddTo(BatchOfOperations& batch)
{
batch.Combine(this);
}
private:

/* adds op to the pointers we store
*/
void Push(Operation* op);

/* retrieves the pointers to Operation objects stored in op
and add them to the ones we store.
Considering our ownership policy should at least empty op of
all its pointers.
This is one of the places where I am uncomfortable with the
ownership policy: Should I assume that op has been dynamically
allocated and simply delete it. This seem in accordance with
the semantics, and furthermore safe: Since void
Add(Operation*& op) takes a non const reference to a pointer,
it guaranties that the object pointed to by op cannot have
automatic lifespan, right? Or I am utterly lost and making that
up?
*/
void Combine(BatchOperation* op);

};

I'm not too sure about the semantics of combining, either.

Quote:
All in all I find the semantics fragile at first view and
somewhat unnatural,

Definitely. I don't know why you want to impose unique
ownership of anything, but if that is a requirement, then
std::auto_ptr is your answer. If you don't need any particular
ownership semantics (which is off hand what I would expect),
garbage collection is the easiest solution, followed by shared
ownership using boost::shared_ptr, followed by shared ownership
using some in house reference counted pointer. (Note, however,
that the reference counted pointer solutions make it more
difficult, if not impossible, to use Operation which aren't
dynamically allocated. That's not usually a problem, but you
never know.)

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34


[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]


Back to top
ravinderthakur@gmail.com
Guest





PostPosted: Wed Sep 28, 2005 11:03 am    Post subject: Re: Problem with design of a class: ownership of data, copy Reply with quote

Quote:
protected:
virtual void AddTo(BatchOfOperations* batch)
{
batch->Push(this);

}

Problem!!! your base class has a knowledge of the derived class. this
is cretainly wrong your operation class SHOULD not be aware of the
derived class BatchOfOperations.The problem is that your base class
objects knows about the derived class and this is leading to these
ownership issues.


Here is the modified class:

class Operation {
public:
virtual ~Operation(){};
virtual operator()(BaseFile &) = 0; // performs the operation
on Basefile
virtual Operation* Clone() = 0;
};

//this class is a Operation class and contains the operation subobjects
that
//all will be treated as a single operation.that's why it is being
derived from
//operation class
class BatchOfOperations : public Operation {
/* BatchOfOperations will delete the Operation */
explicit BatchOfOperations(Operation* op) {
//now we have the owner ship of op
Add(op);
}
void Add(Operation* op) {
//here the op should be added to the current class and we should have
the //responsibility of deleting the object. So the caller
is expected to pass a pointer to
//the copy of the object if he wants to retains a copy of the object
with himselft.
//Though I don't think anybody will do this ever.
this.add(op);
}

protected:
void AddTo(BatchOfOperations* batch) {
//this should be implemented in such a way so that after the function
call we should be //the owner of the operations in batch. Again if
anybody wants to have a copy, then he //can create a copy of Batch and
can keep it with himselft and pass us other copy. Again I //doubt if
anybody will ever need this.
this.combine(batch);
batch.makeEmpty();
}
private:

void Combine(BatchOperation* op);
//write the combine so that we have the owner ship of operations in
batch and after the //combination clear the entries in op.
};


thanks
rt


[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]


Back to top
Gerhard Menzl
Guest





PostPosted: Wed Sep 28, 2005 11:06 am    Post subject: Re: Problem with design of a class: ownership of data, copy Reply with quote

Martin wrote:

Quote:
I want a class (or rather a hierarchy of classes) to implement the
different operations which can be performed on a file. One of its uses
would be inside undo/redo stacks - for an editor. If you think this
design is stupid from the start, please say so Smile.

Are you familiar with "Design Patterns" (also known as the GoF = Gang of
Four book)? If the answer is no, you will be pleased to learn that your
design corresponds more or less exactly to what is described there as
the Command pattern. In other words, it is anything but stupid.

Quote:
So my starting point would be a base abstract class Operation. Each
specific Operation ( Delete, Paste, edition of a field... ) would be
implemented in a derived class.

I also need a way to batch the elementary operations into more complex
ones. The undo/redo stacks will have a "batching state" controlled
through BeginBatch() EndBatch() calls, but I am very hesitant as to
the design of these batches at the level of my Operation class.

I could create a BatchOfOperations class derived from Operation to
implement those (since batches are operations themselves and must be
stored in the undo/redo stack):

This is an elegant idea and known as the Composite pattern. Admit it,
you *do* know "Design Patterns". *g*

Quote:
First option I see is simply to pass pointer to Operation objects to
the batch, and store them. I have some doubts about the ownership
policy, and how to maintain it though. In what follows the batch takes
ownership of the operation objects. The semantics is not very natural
to say the least, since I end up manipulating pointer to Operation
objects where Operation objects would make more sense.

Dynamic polymorphism does not work without indirection. You cannot
bundle objects of an unbounded set of different types together. Pointers
*do* provide the natural semantics for this situation. This, as you have
observed correctly, leaves the ownership problem. Fortunately, there are
tried and trusted idioms in C++ to solve it.

Quote:
class Operation
{
public:
virtual ~Operation(){};

/* performs the operation on Basefile& file,
Might invalidate the Operation object when called (make it an
empty operation):
In case of a paste operation for instance, ownership of the
data stored in the operation might actually have been
transferred to the file for the sake of optimization, and so
the operation should not be able to touch it anymore.
*/

How do you implement undo/redo if the Operation doesn't remember what it
pasted and is not allowed to touch the file anymore?

Quote:
virtual operator()(BaseFile & file) = 0;

You are missing a return type here.

Quote:

protected:
virtual void AddTo(BatchOfOperations* batch)
{
batch->Push(this);

}

I can see what you want to achieve by this, but you should be aware that
it introduces a mutual dependency between Operation and its derived
class BatchOfOperations. In general, this is something that should be
avoided. The GoF have addressed this problem in the Implementation
section of the Composite pattern (p. 168f).

Quote:
private:
//non copyable because the semantics don't fit.
Operation(Operation&);
Operation& operator=(Operation&);
};
class BatchOfOperations : public Operation
{

/* each BatchOfOperations stores pointers to the Operations
it groups, it is responsible for deleting them in the
destructor, or possibly when operator() is called */

explicit BatchOfOperations(Operation*& op)
{
Add(op);
}

/* adding Operation *op to our batch
the batch takes ownership of the Operation object op:
*/
void Add(Operation*& op)
{
op->AddTo(this);
op = 0;// make sure this pointer won't be used again
}

This seems fragile to me. Correct me if I am wrong, but whether an
operation can be repeated, undone, redone, or whether it "expires" after
execution, depends on the type of the operation. Therefore the semantics
should be defined in the concrete operation class. BatchOfOperations
cannot possibly know whether operator() invalidates the object, as it
only operates on the base class interface.

Quote:
protected:
void AddTo(BatchOfOperations& batch)
{
batch.Combine(this);
}
private:

/* adds op to the pointers we store
*/
void Push(Operation* op);


/* retrieves the pointers to Operation objects stored in op
and add them to the ones we store.
Considering our ownership policy should at least empty op of
all its pointers.
This is one of the places where I am uncomfortable with the
ownership policy: Should I assume that op has been
dynamically allocated and simply delete it. This seem in
accordance with the semantics, and furthermore safe: Since
void Add(Operation*& op) takes a non const reference to a
pointer, it guaranties that the object pointed to by op
cannot have automatic lifespan, right? Or I am utterly lost
and making that up?
*/

No, there is no such guarantee. Assuming Paste is derived from
Operation, a client could maliciously write:

void f ()
{
Paste paste (/* constructor arguments here*/);
Operation* po = &paste;
BatchOfOperations batch;
batch.Add (po);
}

If you want to prevent a class from being instantiated automatically,
make its constructors private and provide a static factory function, or
a factory class. See Item 27 in Scott Meyers' "More Effective C++" and
the creational patterns in "Design Patterns" for details.

Quote:
void Combine(BatchOperation* op);

};

Don't you mean

void Combine (BatchOfOperations* op)

?

Quote:
A variant would be to clone every Operations I want to store in the
batch, the result is much better semantically I think, but I am going
to have problems behind the scene.
The data needed to perform certain operation is extensive, a paste
operation for instance will have to store the piece of the file to
paste (and the files the editor is manipulating are relatively big,
typically 10 or 20 Mo of data). If I want to maintain the integrity of
every copy of an Operation object, I'll have to either duplicate that
data (not really an option in this context) or keep a reference count
on it. The same is true when calling operator()(Basefile&) instead of
simply transferring ownership of the data, I will have to use a
reference count, that will necessitate a big rewriting of associated
classes... In the end I wonder if I will not make things more
complicated than they should be (especially considering I am almost
sure there are some problems I am not seeing yet) for a limited
benefit.

No matter whether you clone or not, I would manage the data in separate
classes. When you do this, it is easy to share them via reference count
using non-intrusive smart pointers, such as boost::shared_ptr.

Quote:
An other approach would be to discard the BatchOfOperations class
altogether, and instead make every operation a potential "starting
point" for a batch something like:

class Operation
{
operator()(Basefile& file)
{
ApplyTo(file);
if(nextOp_)
{
(*nextOp_)(file);
}
}
private:
Operation* nextOp_;
/* implements the actual operation: */
virtual DoApply(BaseFile*) = 0;
};

But this doesn't really change a thing, I end up with the same
problems and alternative as with the first two options (in terms of
ownership of the operations linked to, data, copy construction...).

It's also bad design. A class should represent one concept. An operation
and a batch of operations are two distinct concepts.

Ownership of dynamically allocated objects can easily be handled using
appropriate smart pointers. For simple ownership handover (i.e. when the
original owner loses control), std::auto_ptr is suitable. However, due
to its very semantics, it cannot be used in standard containers, hence
it is probably unsuitable for your purpose. For managing shared
ownership using reference counts (the last one to leave turns off the
light), boost::shared_ptr is a good alternative. However, considering
the recursive nature of the Composite pattern (operations own
operations), you would have to take care to avoid circular references.

--
Gerhard Menzl

#dogma int main ()

Humans may reply by replacing the thermal post part of my e-mail address
with "kapsch" and the top level domain part with "net".

[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]


Back to top
kanze
Guest





PostPosted: Thu Sep 29, 2005 8:16 pm    Post subject: Re: Problem with design of a class: ownership of data, copy Reply with quote

Gerhard Menzl wrote:

[...]
Quote:
No, there is no such guarantee. Assuming Paste is derived from
Operation, a client could maliciously write:

void f ()
{
Paste paste (/* constructor arguments here*/);
Operation* po = &paste;
BatchOfOperations batch;
batch.Add (po);
}

If you want to prevent a class from being instantiated
automatically, make its constructors private and provide a
static factory function, or a factory class. See Item 27 in
Scott Meyers' "More Effective C++" and the creational patterns
in "Design Patterns" for details.

That's fine for Paste, or any Operation that you write. But the
goal here, I think, is that any class derived from Operation
must be allocated dynamically. And I don't see how to impose
that.

The obvious solution, of course, is to use garbage collection,
and let the garbage collector sort things out. Otherwise,
document the restriction (IMHO, using a smart pointer, like
boost::shared_ptr, in the interface is very clear
documentation), and let code review ensure that no one has
ignored the documentation.

--
James Kanze GABI Software
Conseils en informatique orientée objet/
Beratung in objektorientierter Datenverarbeitung
9 place Sémard, 78210 St.-Cyr-l'École, France, +33 (0)1 30 23 00 34


[ See http://www.gotw.ca/resources/clcm.htm for info about ]
[ comp.lang.c++.moderated. First time posters: Do this! ]


Back to top
Display posts from previous:   
Post new topic   Reply to topic    C++Talk.NET Forum Index -> C++ Language (Moderated) All times are GMT
Page 1 of 1

 
Jump to:  
You cannot post new topics in this forum
You cannot reply to topics in this forum
You cannot edit your posts in this forum
You cannot delete your posts in this forum
You cannot vote in polls in this forum


Powered by phpBB © 2001, 2006 phpBB Group
SEO toolkit © 2004-2006 webmedic.