Jacob wrote:
I am trying to find the best way of documenting (in code and
comments) ownership of secondary contained objects. This is my
current status, and I would appreciate feedback on it:
This is not hard. You simply comment the member variable as "owned" or
"held".
Case 1: When the secondary object is created with the object
and dies with the object.
[snip] Example:
class Person
{
:
private:
Gender gender_;
};
In this case, you don't even have to document that 'gender_' is "owned"
because it cannot be otherwise.
Case 2: When a secondary object exists somewhere else (i.e. is owned
by someone else) but is referred to by the class.
This is where things get interesting. When you give an external object
to a class to hold, always pass it as a pointer:
[snip] class Person {
public:
Person(Person& mother);
private:
Person& mother_;
};
class Person {
Person *d_mother_p; // this person's mother (held)
public:
Person(Person *mother);
};
The main reason why you store 'mother_p' as a pointer (note the '_p'
suffix) is to denote that it is held (although it may be owned which is
why you need to document this fact). Since 'mother' is stored as a
pointer, you should pass it as a pointer both for reasons of symmetry,
and also to document to the reader of client code that the argument
might be held (but is definitely not copied).
For example, in the following snippet
Bar bar;
Foo foo(bar);
//...
it is not clear that 'bar' is held and not owned (via copying) by
'foo'. Hence, it is difficult to spot bugs involving the premature
deletion of the external object 'bar'. However,
Bar bar;
Foo foo(&bar);
//...
clearly hints at the likelyhood that 'bar' is meant to outlive 'foo'.
Your example is especially interesting because your constructor
parameterized by 'mother' is actually expressed as a copy constructor.
I don't know if this was deliberate, but it could certainly lead to
some interesting bugs.
Case 3: Like case 2, but where the secondary object might be 0.
[snip] Example:
class Person {
public:
void setSpouse(Person* spouse);
private:
Person* spouse_;
};
Hapilly, this is the same as Case 2:
class Person {
Person *d_spouse_p; // this person's spouse (held)
public:
void setSpouse(Person *spouse);
// Set this person's spouse to the specified 'spouse'.
};
The fact that 'spouse' may be zero is an implementation detail that you
don't need to document in the interface. However, you might want to
externalize 'spouse_p' in which case you may add
const Person *spouse() const;
// Return this person's spouse, if one exists; otherwise
// return 0.
It's not important that 'spouse_p' can be null. It is important that
your accessor may *return* a null pointer.
Case 4: A secondary object is created by a client and passed into
a class who becomes its owner.
[snip] Example:
class Person {
public:
void setHairstyle(Hairstyle* hairstyle);
private:
Hairstyle* hairstyle_;
};
Person::~Person()
{
delete hairstyle_;
}
This case is also the same as Case 2:
class Person {
Hairstyle *d_hairstyle_p; // this person's hairstyle (owned)
public:
void setHairstyle(Hairstyle *hairstyle);
// Set this person's hairstyle to the specified
'hairstyle'.
};
However, this too is not a very good example. There are really two
cases where an external object may become "owned" by a 'Person'. In
both cases, the point of ownership relates more than anything to the
relative lifespan of the two objects in the caller.
The first case involves value-semantic objects (those which define copy
constructors and assignment operators). In this case, a 'Person' may
"own" an external object by copying its value:
class Person {
Hairstyle d_haristyle;
public:
void setHairstyle(const Hairstyle& hairstyle)
{ d_hairstyle = hairstyle; }
};
Passing by const reference is, by convention, an indication that the
argument may be copied by the other object, and the caller is free to
destroy the argument object at any point relative to the other object:
Foo foo;
{
Bar bar;
foo.setBar(bar);
}
foo.action(); // involves 'bar'
The second case involves relinquishing ownership of a dynamically
allocated resource to another object. However, in this case I would
suggest (as someone else did in this thread) the use of managed
pointers such as 'std::auto_ptr' or a shared pointer. However, be
careful to pass these by *modifiable* reference:
void setConnectionPool(std::auto_ptr<ConnectionPool>& pool);
The implementation of 'setConnectionPool' may take ownership of 'pool'
by making an assignment between 'auto_ptr's, or may chose to delegate
ownership of 'pool' to yet another object.
The usefulness of this technique becomes more apparent when you
allocate dynamic resources from an allocator other than new/delete. In
this case, you can bundle the object and the allocator in a managed
pointer so that the new owner does not need to be aware of the
particular allocator type used to allocate (and deallocate) the object.
(Sadly, 'auto_ptr' does not currently support this kind of interface.)
HTH, /david