Recently I read Francis' excellent 'Exploring Patterns' article in Overload [OL27] and his 'The object of STL containers' in [EXE]. I thought I should share some of my own experience in this area.
During the design and implementation phases of my last project, a requirement emerged for STL containers of pointers to objects. We could not use value based objects, since the objects were complex, and smart pointers or an STL allocator did not appear to offer the solution either.
The following problem and solution description is not a Pattern, but I'm reusing the form provided by the GOF book [GoF] as a framework.
This class allows an STL container to manage a set of objects, the design of which dictates that their life-time has to be managed in a special way which is different from the STL's by-value semantics. This design introduces an implicit contract with the Client that they must manage the life-time of the Objects.
In the project there were several (three or four) instances where a 'Manager' class had a set of objects. The life-time of the objects was such that they could not be copied without considerable design and run-time cost. These objects were typically resource hungry, often with a life-time of their own because. they created threads or processes for their own internal use. Copies were hard to create, if it was just to move the object in memory, especially where resources were mutexes, which by definition are for granting exclusivity to another resource. Also temporary copies are a complete anathema to the object as they may cause resource scheduling which would absorb lots of CPU time.
A Smart or Reference Counted pointer might not work in these circumstances because the object could not be copied and there would only be one of each Object anyway.
An STL allocator could not be used as it just wants to create and manage memory and does not know when a new object is needed versus a copy of an old one.
This solution is applicable when:
-
an STL container provides the required semantics for the containment of the Objects
-
the cost of creating copies of the an object is large
-
the implementation of either a Smart Pointer or an Allocator for the STL list would not solve the issues
-
Creation of the Object can be done at the same time as it is added to the Container
-
Destruction of the Object can be done at the same time as it is removed from the Container
-
The creation and destruction of the Object can be done from one function each within the Manager.
-
The Object has a complex set of parameters required to define its initial state.
Manager: This class includes an attribute of type:
<list <ShallowPointer< Object > >
This class is responsible for the memory management of the Objects, specifically, it must create 'new' objects to add to the container and 'delete' them off the container.
Shallow Pointer: This is a template class that simply:
-
dereferences the Object for the Manager
-
forwards ('==' & '<' from the STL to the Object)
It does not perform any memory or life-time management.
Object: This has to provide the operators required by the STL container, which are forwarded by the ShallowPointer template class. It abdicates responsibility for its life-time management to the Manager.
ObjectParam: The Parameters required to instantiate an Object. This helps but is not a mandatory part of this design. This also confers the added property of enforcing non-implicit type conversions for the creation of Objects, See [Meyers] 'Item 5'.
It is the Manager that has to manage the life-time of the Objects. The Manager needs to create new instances of Objects as they are added onto the list and is required to delete the Objects as they are removed from the list. This is best achieved by having one 'AddObject' and one 'DelObject' function in the Manager, with these being the only places where the list is added to or has Objects removed from it. This is the implicit contract between the ShallowPointer and its Client.
The life-time of the object is tightly coupled with its tenure in the Container. This is an undesirable side-effect for which this design is very unattractive. Also the management of the Object is in the hands of the Client rather than being hidden from the Client, another unattractive feature of this design.
I have put the bodies into the class definition, but in the source code there are separate header and implementation files. template <class T> class ShallowPointer { public: ShallowPointer(T *pObj) : pObject(pObj) {} ; ShallowPointer( const ShallowPointer &rhs) { // Shallow Copy pObject = rhs.GetPointer(); } ~ShallowPointer() throw() { pObject = 0 ; // Shallow Delete } // De-reference this Pointer to return // a pointer to the Object of type T as // a T *const - this should be private? T *const GetPointer() const { return pObject ; } // De-reference this Pointer to return // a pointer to the Object of type T as // a T *const. Thus providing // 'transparent' access to members of T // from the List without adding them to // this class. T *const operator ->() const { return GetPointer() ; } T *const operator *() const { return GetPointer() ; } // Used by List::sort(), Forward to // Object int operator <( const ShallowPointer &rhs) { return GetPointer()-> operator<(*rhs.GetPointer()) ; } // Used by List::, Forward to Object int operator ==( const ShallowPointer &rhs) { return GetPointer()-> operator==(*rhs.GetPointer()) ; } private: // Hide from Compiler, STL & Clients ShallowPointer(); const ShallowPointer<T> & operator=( const ShallowPointer<T> &right); private: // Attributes T *pObject ; // Pointer to our Object };
// The required operations in Manager:: // Construct & add to the Container based // on a set of parameters void Manager::AddObject( ObjectParam op ) { queue.push_back( new Object(op) ) ; // post-condition: list is in // priority order queue.sort() ; } // Find and removed the specified Object // from the Conainer & destruct the // Object void Manager::DelObject( Object *const pObj ) { list<ShallowPointer<Object> >::iterator i; for( i = queue.begin(); i != queue.end(); i++ ) { if( (*i).GetPointer == pObj ) { // Compare Pointers not Objects // Remove from Container queue.erase( i ) ; delete pObj ; break ; } } }
This sample code, relies on there being two member functions in Object:
-
bool Object::IsCal() const ;
-
void Object::StartJob() ; // not const!!!
It uses the first function to choose an Object of interest and then uses the second to send a message to the Object.
Object *const Manager::StartCalibration() { // pre-condition: // list is in priority order queue.sort() ; Object *pJob(0) ; list<ShallowPointer<Object> >::const_iterator i ; for( i = queue.begin(); i != queue.end(); i++ ) { if( !pJob && (*i)->IsCal() ) { // Find the highest priority cal. Job // Keep Pointer [see limitations] pJob = *i.GetPointer() ; break ; } } if( pJob ) { // Start the Process pJob->StartJob() ; } return pJob ; }
In this project the prime use was that of a Queue Manager (the Manager class), which was required to maintain an ordered list of Jobs (the Object class). Each Job had a Priority and a Function. The Priority was higher for Jobs submitted by Acquisition and lower for Jobs submitted by the User (for re- processing). Job Functions were: Calibration, Legacy or Signal Processing (User defined from supplied building blocks). It was decided to use an STL list and use the sort member function to provide the required 'ordering'. Each Job had to manage the resources required to execute its process. This was achieved by having a thread per Job to manage its state machine and a process per job for the actual processing. Unix Signals and Process Control functions were used to synchronise and manage the Jobs, with messages and call-backs to the Queue Manager to keep the Manager informed. The Queue Manager was single-threaded, so STL access was simple.
The reason an STL list was used rather than an STL vector, was simply that someone had defined the STL list in Rational Rose and none of the other containers!
ShallowPointer::GetPointer() should be private, having it in public scope exposes the fact that internally this class is managing a pointer. We should also being hiding this implementation detail.
What I think this solution lacks is controlled, and hidden, object lifetime management. This design is too ad-hoc to leave it to STL and the Client of the Object (Manager). What I think is required is another class to control the life-time management of the Objects, similar to a Factory perhaps, but then the Factory may have to become the container too, and provide the required iterator classes too.
This is a very lightweight implementation and as I write this, I can see increasingly that Francis is correct in asserting that a 'Smart Pointer' approach may be the correct way forward. See: [OL27] 'Exploring Patterns' and [EXE] 'The Object of STL Containers'. This solution needs to be extended to incorporate more of the semantics of the Smart Pointer.
As Kevlin amusingly observed at the excellent ACCU Forum 98, if you have two choices always pick the third. In this case, our choices are By-Value and Smart Pointers, so we should pick the ShallowPointer solution. This may be the worst of both worlds as it lacks the simplicity of the STL's by-value semantics and does not provide the controlled management of a Smart Pointer.
This solution may only be half-baked, but it has worked successfully for us. It was expedient at the time and has the merits of being simple enough to be tested and trusted, although it does place a high burden on the Client to use. This is probably too high a price. As Scott Meyers observes in Meyers the job of the class designer should be hard (and it wasn't in this case) so that the role of the Client is simple (it isn't in this case). Thus I can only conclude that this is a bad design and should be taken as an example of such.
Overload Journal #28 - Oct 1998 + Design of applications and programs
Browse in : |
All
> Journals
> Overload
> 28
(10)
All > Topics > Design (236) Any of these categories - All of these categories |