20 Jan 2007

Nebula3 ref-counting and smart pointers.

C++ only offers automatic life time management for objects created on the stack. When the C++ context is left, stack objects will be destroyed automatically:

{
// create a new object on the stack
MyObject obj;

// do something with obj...

// current context is left, obj is destroyed automatically
}

When creating an object on the heap, the object has to be destroyed manually, otherwise a memory leak will result:

{
// create an object on the heap
MyObject* objPtr = new MyObject;

// do something with obj...

// need to manually destroy obj
delete obj;
}

This gets all much more complicated, when more then one "client" needs access to a C++ object, because then, ownership rules must be defined (the owner would be responsible for deleting an object, all other clients just "use" the object).

In a complex software system, this ownership management gets tricky very quickly. An elegant solution to this problem is refcounting. With refcounting, no ownership must be defined, since each "client" increments a reference count on the target object, and decrements the refcount when it no longer needs to access the object by calling a Release() method. When the refcount reaches zero (meaning, no client accesses the object any more), the object is destroyed. This fixes the multiple client scenario, but still requires the programmer to manually call the Release() method at the right time.

Smart pointer fix this second problem as well. A smart pointer is a simple templated C++ object which points to another C++ object, which manages the target refcount on creation, destruction and assignment. Other then that a smart pointer can just be used like a raw pointer, except that it fixes all the dangerous stuff associated with raw pointers.

Lets take a look at how the above code would look in Nebula3:

{
// create a C++ object on the heap
Ptr< MyObject > obj = MyObject::Create();

// do something with obj
obj-> DoSomething();

// at the end of context, the smart pointer object is destroyed
// and will release its target object
}

With smart pointers, a heap object handles exactly like a stack object, no extra care is needed for releasing the object at the right time. Smart pointers also fix the cleanup problem with arrays of pointers. If you want to create a dynamic array with raw pointers to heap objects, you must take care to delete the target objects manually before destroying the array, because a raw pointer has no destructor which could be called when the array is destroyed. By creating an array of smart pointers, this problem is solved as well. When the array is released, it will call the destructors of the contained smart pointers, which in turn will release their target objects:

{
// create an array of smart pointers
Array< Ptr< MyObject >> objArray;

// create objects and append to array
int i;
for (i = 0; i < 10; i++)
{
Ptr< MyObject > = MyObject::Create();
objArray.Append(obj);
}
// when the current context is left, the array is destroyed
// which destroys all its contained smart pointers, which in turn
// destroy their target object...
}

That's it, simple and elegant. With smart pointers you can work with heap objects just as they were stack objects!

Refcounting and smart pointers are not perfect however. They fail on cyclic dependencies (when 2 objects point to each other). There seems to be no clean and easy fix to this. Thankfully, in a well-designed software system cyclic dependencies are rarely necessary.