Delphi Clinic | C++Builder Gate | Training & Consultancy | Delphi Notes Weblog | Dr.Bob's Webshop |
|
That's me over there
Last column I gave an overview of use counting.
It took us along what I learned on that subject from Andrew Koenig while he did two presentations at C++ World Conference 1999 in Miami, Florida.
That was not the only subject he presented, actually, he mixed use counting with a templatized version of handle-body idiom.
In this installment I'll run you through what Andrew told us about that.
You might want to have the contents of lost+found 11 handy.
I am using the Use class as outlined in that lost+found installment.
Introduction
Coplien tells us in 'Advanced C++ Programming Styles and Idioms' that the handle-body idiom 'may be used to decompose a complex abstraction into smaller, more manageable classes' and that it 'may reflect the sharing of a single resource by multiple classes'.
Finally, he concludes that 'the most common case is for reference counting'.
In lost+found 11 you have seen a more or less classic approach to this idiom.
Another method for reference counting is using counted pointers.
An example is:
class ObjRef { public: ObjRef(Object* objptr = 0) : ptr(objptr), use() {} ObjRef(const ObjRef& that) : ptr(that.ptr), use(that.use) {} ObjRef& operator=(const ObjRef& that) { if (this != &that) { if (use.Unique() == true) delete ptr; use = that.use; ptr = that.ptr; } return *this; } ~ObjRef() { if (use.Unique() == true) delete ptr; } //---------------------------------------------------------- // Magic tricks! Obj& operator*() const {return *ptr;} Obj* operator->() const {return ptr;} //---------------------------------------------------------- // I want my own Object! void MakeUnique() { if (use.Unique() == false) { use = Use(); ptr = new Object(*ptr); } }; private: Obj* ptr; // The object referred to Use use; // lost+found 11 };Magic involved
ObjRef or(new Object); or->DoOne();If you later decide to add DoTwo, you do not need to add DoTwo to the ObjRef class, but simply use:
or->DoTwo();The only funny thing is that the use of ObjRef looks like a normal object, but when using it it looks like a pointer.
Templatizing it
A counted pointer is very suited to be templatized.
Here is how:
templateDrawbackclass CountedPointer { public: CountedPointer(T* objptr = 0) : ptr(objptr), use() {} CountedPointer (const CountedPointer & that) : ptr(that.ptr), use(that.use) {} CountedPointer & operator=(const CountedPointer & that) { if (this != &that) { if (use.Unique() == true) delete ptr; use = that.use; ptr = that.ptr; } return *this; } ~ CountedPointer () { if (use.Unique() == true) delete ptr; } //---------------------------------------------------------- // Magic tricks! T& operator*() const {return *ptr;} T* operator->() const {return ptr;} //---------------------------------------------------------- // I want my own Object! void MakeUnique() { if (use.Unique() == false) { use = Use(); ptr = new T(*ptr); } } private: T* ptr; // The object referred to Use use; // lost+found 11 };
CountedPointerWhat happens if we execute:cpb = new CountedPointer (new Base); CountedPointer cpd = new CountedPointer (new Derived);
cpd.MakeUnique();What happens is that now the cpd points to a sliced copy of the Derived object we originally put in the CountedPointer, since it executes ptr = new T(*ptr). Since T is of type Base here, we will call new Base instead of new Derived.
First solution
The first solution to the drawback is to introduce a Clone member in the class referred to:
class Base { public: virtual Base* Clone() const {return new Object(*this);} }; class Derived: public Base { public: virtual Base* Clone() const {return new Derived(*this);} };However, that means that you almost always must specialize Clone for every class in the class hierarchy. If you forget to do so, the object will still be sliced because the virtual member of one of the base classes in the hierarchy is called. That defies the non-intrusiveness of the Use class, and, as noted in lost+found 11, you sometimes do not have the means to modify a class.
Second solution
The second solution is to provide a Clone template:
templateWe could have used:T* Clone(const T* p) { return p->Clone(); }
templatebut that is dangerous. If we forget to specialize the Clone template then we will get slicing behavior again without any error messages.T* Clone(const T* p) { return new T(*p); }
We have to adjust the MakeUnique member in this case:
//---------------------------------------------------------- // I want my own Object! void MakeUnique() { if (use.Unique() == false) { use = Use(); ptr = Clone(ptr); } }This solution is better, because we can specialize the template for those objects that do not have a Clone member themselves:
template <> Derived* Clone(Derived* p) { // Derived does not have a Clone return new Derived(*p); }The compiler will warn us if a class does not have the proper Clone member to use. That's a sure sign we must specialize the template function. However, we might end up with a lot of template specializations to make this work. In that case, using a macro to generate the template function specializations might help:
#define SPECIALCLONE(Type)\ template <> Type * Clone(Type * p)\ {\ return new Type (*p);\ }