Topic : Smart pointer templates in C++
Author : David Harvey
Page : << Previous 2  Next >>
Go to page :


return --Count; }
        T* operator->()      { return &myT; }
private:
        T               myT;
        unsigned        Count;
};

A couple of ASSERTs check that no references exist for the object on deletion, and that decrementing the count is only done when the count is greater than zero. Both members which update the reference count return the new value of the count.
There is an inconsistency between member functions defined on the template itself, and those we can access on the wrapped instance by virtue of the overloaded dereferencing operator. Given an instance of the template class, we must mix the dot and arrow notations depending on what is being called:

Counted<Thing>  t;
t.IncRef();     // Call Template<Thing>::IncRef()
...
t->Foo();       // Call Thing::Foo()
...
t.DecRef();

Another drawback arises because the overloaded dereferencing operator applies to an object, not a pointer. If we decided to pass around pointers to Counted<Thing>, calls to the wrapped instance would need to dereference the pointer first. This is inconvenient.
void TryIt(Counted<Thing>* p) {
        p->IncRef();    // Call Template<Thing>::IncRef()
        (*p)->Foo();    // Call Thing::Foo()
        p->Foo();       // NB Won't work!
        p->DecRef();
}

However, we can hide much of this behind a second template class to manage automatic reference counting and object deletion. Instances of this class act like object variables in Smalltalk and similar languages, in which a variable is effectively always a reference of some sort (in C++ terms, a reference or pointer), and never a 'whole' object on the stack or in static storage. This restriction on the way objects can be created and used has advantages over C++'s position of complete compatibility with C. In particular, assignment of one object variable to another simply means sharing a reference, not copy of an object: the complication of copy constructors and assignment operators can be ignored. Among more recent languages to adopt this model rather than a 'declare-anywhere' usage are Borland's Delphi, and Java, a rational C++ from Hewlett Packard.
We'll start with a variation on the reference-counted template, using a pointer to the embedded instance of the parameter class, initialised from a pointer passed to the constructor. An assertion in the body of the constructor offers some protection against invalid initialisation.

template <class T> class Objvar;

template <class T>
class Counted
{
        friend class ObjVar<T>;
private:
        Counted(T* pT) : Count(0), my_pT(pT)
                           { ASSERT(pT != 0); }
        ~Counted()         { ASSERT(Count == 0); delete my_pT; }

        unsigned GetRef()  { return ++Count; }
        unsigned FreeRef() { ASSERT(Count!=0); return --Count; }

        T* const my_pT;
        unsigned Count;
};

Now that this class has become, effectively, a helper class, all members are private, and friendship is granted to the class which will finally implement the object variable itself. This protection could also be managed by declaring the template class Counted within the private section of the declaration of ObjVar. However, current compilers differ in their ability to cope with nested template classes: the solution using friend also has the advantage of simplifying the declaration of the ObjVar class itself.
The Counted class no longer overloads the dereferencing operator. The friend declaration to the corresponding ObjVar class means the pointer to the actual object can be retrieved efficiently. Alternatively we could provide an inline accessor member function to return the pointer to the wrapped instance.

We can now define the object variable class to wrap a pointer to an instances of this counted class. We manage sharing of the reference by overloading assignment and providing a copy constructor. Here is the declaration of the class:

template <class T>
class ObjVar
{
public:
        ObjVar();
        ObjVar(T* pT);
        ~ObjVar();
        ObjVar(const ObjVar<T>& rVar);

        ObjVar<T>& operator=(const ObjVar<T>& rVar);
        
        T* operator->();
        const T* operator->() const;

        friend bool operator==(const ObjVar<T>& lhs,
                        const ObjVar<T>& rhs);

        bool Null() const {return m_pCounted == 0};
        void SetNull() { UnBind(); }

private:
        void UnBind();
        Counted<T>* m_pCounted;

};

In practice, I would expect all the functions to be inline for performance reasons. The following discussion of each of the members presents them out-of-line for ease of reference.
The default constructor creates an ObjVar in which the pointer to the counted object is set to zero, making it in effect the nul object. A member function Null() is provided to test for this, and a SetNull() function to lose the binding to the current object.

The one-argument constructor creates (using new) an instance of the counted class with the passed pointer, then increments the reference count on this object. The destructor calls UnBind (described below):

template<class T>
ObjVar<T>::ObjVar()
: m_pCounted(0) {}

template<class T>
ObjVar<T>::ObjVar(T* pT)
{
        m_pCounted = new Counted<T>(pT);
        m_pCounted->GetRef();
}

template<class T>
ObjVar<T>::~ObjVar()
{
        UnBind();
}

The member function UnBind is called whenever the ObjVar instance loses a reference to the wrapped counted instance. If in the process the reference count becomes zero, the wrapped instance can safely be deleted. The ObjVar's pointer to the counted instance is set to zero to indicate that the current variable now references the Nul object.
template<class T>
void ObjVar<T>::UnBind()
{
        if (!Null() && m_pCounted->FreeRef() == 0)      
                delete m_pCounted;
        m_pCounted = 0;
}

The overloaded dereferencing operator simply returns the object held in the currently referenced counted instance. We throw an exception if an attempt is made to apply the operator to a null object variable.
template<class T>
T* ObjVar<T>::operator->()
{
        if (Null())
                throw NulRefException();
        return m_pCounted->my_pT;
}

template<class T>
const T* ObjVar<T>::operator->() const
{
        if (Null())
                throw NulRefException();
        return m_pCounted->my_pT;
}

Two versions of this operator are provided, one non-const (returning a pointer to the wrapped class), the other const (returning a pointer to const T). This allows us to declare and use constant object variables, through which it is impossible to change the object being wrapped. Note that this does not mean that an object referenced by a constant ObjVar cannot change. Just as with raw C++ pointers and references, another object may hold a non-const ObjVar to the same underlying object.
Things get more complicated in the copy constructor and assignment operator. Remember that copying and assigning mean sharing the reference, not copying the object. For the copy constructor, we must store the pointer to the counted object from the argument to the constructor, and increment its reference count:

template<class T>
ObjVar<T>::ObjVar(const ObjVar<T>& rVar)
{
        m_pCounted = rVar.m_pCounted;
        if (!Null())
                m_pCounted->GetRef();
}

In the assignment operator, we must detach the current counted object, decrement its reference count and delete it if it is zero, then attach the counted object from the argument, and increment its reference count. As always, we must take care to deal properly with self-assignment. The issue is dealt with neatly here by incrementing the argument (right-hand side) reference count

Page : << Previous 2  Next >>