Smart Pointers and Naked Pointers


Clearly, all smart pointers wear clothing.

If you declare a pointer to another object, you've just used a naked pointer. Pointers are used to refer to another object, but they don't convey enough information. Anything declared on the heap must be referenced by at least one other object or it can never be freed, causing a memory leak. It is common for an object on the heap to be referred multiple times by other objects in the code. A good example of this is a game object like a clock. A pointer to the clock will exist in the game object cache, the collision system, the animation system, and even the sound system.

If you use naked pointers you must remember which objects implicitly own other objects. An object that owns other objects controls their existence. Imagine a ship object that owns everything on the ship. When the ship sinks, everything else is destroyed along with it. If you use naked pointers to create these relationships you have to remember who owns who. This can be a confusing or even impossible task. You'll find that using naked pointers will quickly paint you into a corner.

Smart pointers, on the other hand, hold extra information along with the address of the distant object. This information can count references, record permanent or temporary ownership, or perform other useful tasks. In a sense an object controlled by a smart pointer "knows" about every reference to itself. The horrible nest of naked pointers evaporates, leaving a simple and foolproof mechanism for handling your dynamic objects.

Reference Counting

Reference counting stores an integer value that counts how many other objects are currently referring to the object in question. Reference counting is a common mechanism in memory management. DirectX objects implement the COM based IUnknown interface, which uses reference counting Two methods that are central to this task are AddRef() and Release(). The following code shows how this works:

 MySound *sound = new MySound; sound->AddRef();                   // reference count is now 1 

After you construct a reference counted object, you call the AddRef() method to increase the integer reference counter by one. When the pointer variable goes out of scope, by normal scoping rules or by the destruction of the container class, you must call Release(). Release() will decrement the reference counter and destroy the object if the counter drops to zero. A shared object can have multiple references safely without fear of the object being destroyed, leaving bad pointers all over the place.

Gotcha

Good reference counting mechanisms automatically delete the object when the reference count becomes zero. If the API leaves the explicit destruction of the object to you, it's easy to create memory leaks—all you have to do is forget to call Release(). You can also cause problems if you forget to call AddRef() when you create the object. It's likely that the object will get destroyed unexpectedly, not having enough reference counts.

Anytime you assign a pointer variable to the address of the reference counted object you'll do the same thing. This includes any calls inside a local loop:

 for (int i=0; i<m_howMany; ++i) {    MySound *s = GoGrabASoundPointer(i);    s->AddRef();    DangerousFunction();    if (s->IsPlaying())    {       DoSomethingElse();    }    s->Release(); } 

This kind of code exists all over the place in every game I've ever worked on. The call to DangerousFunction() goes deep and performs some game logic that might attempt to destroy the instance of the MySound object. Don't forget that in a release build that the deallocated memory retains the same values until it is reused. It's quite possible that the loop will work just fine even though the MySound pointer is pointing to unallocated memory. What's more likely to occur is a terrible corruption of memory.

Reference counting keeps the sound object around until Release() is called at the bottom of the loop. If there was only one reference to the sound before the loop started, the call to AddRef() will add one to the sound's reference count, making two references.

DangerousFunction() does something that destroys the sound, but through a call to Release(). As far as DangrousFunction() is concerned, the sound is gone forever. It still exists because one more reference to it, through MySound *s, kept the reference count from dropping to zero inside the loop. The final call to Release() causes the destruction of the sound.

SmartPtr

If you think calling AddRef() and Release() all over the place might be a serious pain in the ass, you're right. It's really easy to forget an AddRef() or a Release() call, and your memory leak will be almost impossible to find. It turns out that there are plenty of C++ templates out there that implement reference counting in a way that handles the counter manipulation automatically. Here's an example:

 #ifndef SMARTPTR_H #define SMARTPTR_H ////////////////////////////////////////////////////// // //  Code modified from the original, to conform with //  coding standards in the book. // //  Original Code by Sandu Turcan //  Posted on the Code Project: http://www.codeproject.com/ template <class T> class SmartPtr; ///////////////////////////////////////// // IRefCount // is an interface for reference counting // Classes can implement it themselves, // or SmartPtr will provide its internal implementation of IRefCount template <class T> class IRefCount {     friend class SmartPtr<T>;     protected:     virtual void AddRef() = 0;     virtual void Release() = 0;     virtual T * GetPtr() const = 0; }; //=============================================== ///////////////////////////////////////// // IRefCountImpl // is a standart implementation of IRefCount // To use it just derive your class from it: // class CMyObject : public IRefCountImpl<CMyObject> { ... }; // Reminder: implementing IRefCount is optional but it would reduce // memory fragmentation. template <class T> class IRefCountImpl   : public IRefCount<T> { private:     int m_Count; protected:     virtual void AddRef() { m_Count++; }     virtual void Release()     {    assert(m_Count>=0);         m_Count-;         if(m_Count<=0)         {             Destroy();         }    }    virtual T * GetPtr() const { return ((T *)this): }    virtual void Destroy() { if (GetPtr()!=NULL) delete GetPtr(); }    IRefCountImpl() { m_Count = 0; } }; //============================================================= ///////////////////////////////////////// // SmartPtr template <class T> class SmartPtr { private:     IRefCount<T> *m_RefCount;     /////////////////////////////////////////     // RefCounter     // An internal implementation of IRefCount     // for classes that don't implement it     // SmartPtr will automatically choose between its internal and     // class' implementation of IRefCount     class RefCounter : public IRefCountImpl<T> {         private:         T *m_Ptr;         protected:         virtual T * GetPtr() const { return m_Ptr; }         virtual void Destroy()     { delete this; }         public:         RefCounterd *ptr)  { m_Ptr = ptr; }         virtual ~RefCounter()     { IRefCountImpl<T>::Destroy(); }     };     // this method is called if T does not implement IRefCount     void Assign(void *ptr)     {        if(ptr==NULL)            Assign((IRefCount<T> *)NULL);        else        {             Assign(new RefCounter(static_cast<T *>(ptr)));        }     }     // this method is picked over Assign(void *ptr)     // if T implements IRefCount.     // This allows some memory usage optimization     void Assign(IRefCount<T> *refcount)     {         if( refcount!=NULL )             refcount->AddRef();         IRefCount<T> *oldref = m_RefCount;         m_RefCount = refcount;         if ( oldref!=NULL )            oldref->Release();     } public:     SmartPtr()                        { m_RefCount = NULL; )     SmartPtr(T * ptr)                 { m_RefCount = NULL; Assign(ptr); }     SmartPtr(const SmartPtr &sp)      { m_RefCount = NULL; Assign(sp.m_RefCount); }     virtual ~SmartPtr()               { Assign((IRefCount<T> *)NULL); }     T *GetPtr() const { return (m_RefCount==NULL) ? NULL : m_RefCount->GetPtr(); }     // assignment operators     SmartPtr& operator = (const SmartPtr &sp) { Assign(sp.m_RefCount); return *this; }     SmartPtr& operator = (T * ptr)   { Assign(ptr); return *this; }     // T access and const conversion     T * operator ->()          { assert(GetPtr()!=NULL); return GetPtr(); }     operator T* () const       { return GetPtr(); }     // utilities     bool operator !()                     { return GetPtr()==NULL; }     bool operator ==(const SmartPtr &sp)  { return GetPtr()==sp.GetPtr(); }     bool operator !=(const SmartPtr &sp)  { return GetPtr()!=sp.GetPtr(); } }; #endif 

Here's an example on how to use this template:

 ///////////////////////////////////////// // SmartPtr.cpp // // Examples on how the SmartPtr template classes are used. // #include "stdafx.h" #include "assert.h" #include "smartptr.h" class CMyObject {    char *name; public:    CMyObject(char  *aname)  { name = aname;   printf("create %s\n",name); }    virtual ~CMyObject()     { printf("delete %s\n",name); }    void  print()            { printf ("print %s\n",name); } }; SmartPtr<CMyObject> f1(char *name) {     return SmartPtr<CMyObject>(new CMyObject(name)); } void f2(CMyObject *o) {     printf("(print from a function) ");     o->print(); } int main(void) {    SmartPtr<CMyObject> ptrl(new CMyObject("1"));  // create object 1    SmartPtr<CMyObject> ptr2 = new CMyObject("2"); // create object 2    ptr1 = ptr2;             // destroy object 1    ptr2 = f1("3");          // used as a return value    ptr2 = NULL;             // destroy object 3    f2(ptr1);    // BAD USEAGE EXAMPLES....    //    // CMyObject o1;    // ptr1 - &o1;       // DON'T ! It's on the stack....    //    // CMyObject *o2 = new CMyObject;    // ptr1 = o2;    // ptr2 = o2;        // DON'T ! unless CMyObject implements IRefCount    //                   // try to use ptr1 = ptr2 instead, it's always safe;    // You can even use SmartPtrs on ints!    SmartPtr<int> a(new int);    SmartPtr<int> b(new int);    *a = 5;    *b = 6;     // No leaks!!!! Isn't that cool...     return 0; } 

The template classes use overloaded assignment operators to handle the calls to AddRef() and Release(). As long as the SmartPtr object is in scope and you behave yourself by avoiding the bad usage cases you won't leak memory and you won't have to worry about objects getting destroyed while you are still referencing them from somewhere else.

This smart pointer will not work in every circumstance. One limitation is that it will not work in a multithreaded environment. There are two reasons. First, the IRefCountImpl class would have to be changed to lock access to m_Count, which is used to count references. If two threads attempted to increase or decrease the reference count at exactly the same time you'll end up with a reference count with the wrong value. Secondly, there is no safe access to the memory block in a multithreaded executable. Multiple threads can stomp through the bits at their leisure.

Best Practice

Don't ignore multithreaded access to shared memory blocks. You might think that the chances of two threads accessing the shared data are exceedingly low, and convince yourself that you don't need to go to the trouble of adding multithreaded protection. You'd be wrong, every time.

It is a good idea to have a separate template for multithreaded smart pointers since they run slower. Use the single threaded version in as many places as you can because it's more efficient. Save the multiple threaded version only for the cases you need it. I'll leave the happy task of making the SmartPtr template multithread-aware to you.

When you design your own smart pointer system, you should create many classes that perform limited scope tasks instead of one monolithic class that attempts to fill every role.




Game Coding Complete
Game Coding Complete
ISBN: 1932111751
EAN: 2147483647
Year: 2003
Pages: 139

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net