Reference-count garbage collection is a powerful technique for managing memory that helps prevent objects from being deleted accidentally or more than once. The technique is not limited to C++ code and, despite its name, is unrelated to the C++ concept of reference variables. Rather, the term means that we maintain a count of all ``owning references'' to an object and delete the object when this count becomes zero.
Garbage collection in Iotr is accomplished by the collaboration of two classes, IotrRefCount and Handle. IotrRefCount is a base class of all reference-counted classes (i.e., most classes) in Iotr. Handle is a class whose instances act like pointers that manage their reference count automatically. The use of the Handle template class to manage reference counts is ubiquitous in the Iotr code, and is highly recommended.
Suppose MyClass
is a subclass of IotrRefCount.
class MyClass : public IotrRefCount { ... };
MyClass
is created, it is given a reference count of 1. MyClass * m = new MyClass; std::cout << m->refs() << std::endl; // will write "1"
m
. int oldRefs = m->refs(); anotherObject->someMethod( m ); if( m->refs() > oldRefs + 1 ) { // anotherObject is now maintaining a reference to m } else { // anotherObject declined to maintain a reference to m }
m
will not be released until all objects relinquish their references to m
. One way that this can be done is through a call to IotrRelease. IotrRelease( &m ); // m's reference count is decremented. If becomes 0 // then m is deleted.
For most method calls, other than constructors, the issue of reference counting never arises. Whenever possible, parameters and return values are C++ "reference variables". For example, the following code fragment defines methods that either return a reference to an object or take a reference as an argument:
class MyOtherClass : public IotrRefCount { ... virtual MyClass & getObject(); virtual void transform( MyClass & m ); };
MyClass * m = new MyClass;
anotherObject->transform( *m );
m
untouched. Similarly, in a call such as MyClass & n = anotherObject->getObject();
n
to alter the reference count of the object returned from MyClass::getObject
. One must be careful that anotherObject
is not deleted within the scope of the variable n
, or else n
will likely refer to a deleted member of anotherObject
.
There are occasions when it is desirable to pass a pointer as a parameter or return a pointer from a function so that the reference count may be manipulated. It is common to use pointers as parameters to constructors.
class MyClass : public IotrRefCount { MyClass( MyOtherClass * obj ); // constructor taking a pointer as parameter ... };
MyOtherClass * anotherObject = new MyOtherClass; MyClass * anObject = new MyClass( anotherObject );
MyClass::MyClass
constructor must increment the reference count of anotherObject
if the new instance of MyClass
maintains a reference to the object pointed to by anotherObject
. The routine may also decline to establish such a reference and leave the reference count untouched. The calling routine may not assume that the reference count will be incremented by MyClass::MyClass
.
Pointers are used as return values of methods primarily when a new object is returned as the result of some calculation.
class MyOtherClass : public IotrRefCount { ... MyClass * newRelatedObject() { return new MyClass( this ); } };
MyOtherClass::newRelatedObject
must decrement the reference count of the new instance of MyClass
or the object will never be deleted. A discussion of the recommended technique for incrementing and decrementing the reference count of an object may be found in the section discussing Handles.
Note that the following code is acceptable.
{ MyClass p; anotherObject->transform( p ); }
p
is created on the stack as an "automatic variable" and is deleted when it goes out of scope, regardless of its reference count. This idiom is useful for creating temporary objects where the code required to manage the reference count is cumbersome. Creating reference-counted objects as automatic variables will be safe as long as you never take the address of these variables. On the other hand, code that does take the address of these variables is almost guaranteed to generate bugs. Consider for example the following. {
MyClass p;
anotherObject->keep( &p ); // Don't do this!
}
p
will be deleted, and the reference kept by anotherObject
will become invalid. The invalid reference will almost certainly cause the program to crash at some other point in the code.
A handle is an object that acts like a pointer that automatically manages the reference count of the object it points to. Handle is a template class. Suppose we define MyClass
as a subclass of IotrRefCount.
class MyClass : public IotrRefCount { ... void method1(); };
Handle<MyClass> object;
Handle<MyClass>
by means of a typedef
. typedef Handle<MyClass> MyClassHandle; MyClassHandle handle;
{
MyClassHandle handle( new MyClass );
...
}
MyClass
is created and bound to the variable handle
. Immediately before the block ends and the variable handle
ceases to exist, the reference count of the object bound to handle
is automatically decremented. If the reference count of the object becomes zero, the object is deleted.
In method calls, handles may be used exactly as if they were pointers of type (MyClass *)
.
class MyClass : public IotrRefCount { public: void method1(); void method2(MyClass &); void method3(MyClass *); }; ... MyClassHandle object( new MyClass ), object2( new MyClass ); object->method1(); object->method2( *object2 ); object->method3( object2 );
Sometimes it is necessary to bind an object to an existing handle, rather than create a new handle. When the object is returned from a method, or by operator new
, a handle may be rebound as follows.
MyClassHandle object; // Refers to nothing if( object ) { // Can't get here. "object" is nil and so evaluates as false. } ... // Rebind the variable "object" object = MyClassHandle( new MyClass ); if( object ) { // Always get here. "object" is not nil and so evaluates as true. }
When an object is passed into a routine as a parameter, a different syntax must be used. This is most typically done in constructors, as in the following example.
class MyOtherClass : public IotrRefCount { protected: MyClassHandle myC; public: MyOtherClass::MyOtherClass( MyClass * c ) { HandleReferTo( myC, c ); } };
c
to indicate that a new reference to the object has been established. There is also a special syntax to be used when a pointer to an object is returned from a function. void MyClass::newObject() { MyClassHandle handle( new MyClass ); ... return HandleAsPointer( handle ); }
MyClass & MyClass::getMyC()
{
return *myC;
}
MyClassHandle object1(new MyClass), object2(new MyClass ); object1 = object2;
In this section, we describe several common idioms involving handles. It is very common to create an object, and then pass it to the constructor of another object, as follows.
MyClassHandle handle( new MyClass ); MyOtherClassHandle otherHandle( new MyOtherClass( handle ) );
handle
. Don't do the following. // Don't do this MyOtherClassHandle otherHandle( new MyOtherClass( new MyClass ) );
MyClass
object will never become zero.
On the other hand, it is possible to avoid assignment of an object to a handle when the object is to be immediately returned from a function.
void MyClass::newObject() { return new MyClass; }
MyClass
and the return statement, then it is advisable to assign the new object to a handle, rather than to a pointer. Otherwise, the MyClass
object might not be deleted if an exception is thrown.
It is common to assign the value of a handle to a C++ reference variable to simplify syntax.
MyClassHandle hObject( anotherObject->newMyClass() ); MyClass & object = *hObject;
As noted above, it is not necessary to create a handle for temporary, local variables. These may safely be allocated as "automatic variables" on the stack, so long as you never use operator &
to take the address of the object. Thus temporary objects may be created using code similar to the following.
MyClass object;
Multiple return values are handled as in the following snippet.
void multiReturn( MyClass *& object1, MyClass *& object2 ) { MyClassHandle obj1( new MyClass ), obj2( new MyClass ); // Do some computations with obj1 and obj2 object1 = HandleAsPointer( obj1 ); object2 = HandleAsPointer( obj2 ); } void callingFunction() { MyClassHandle hObject1, hObject2; { MyClass *object1, *object2; hObject1 = MyClassHandle( object1 ); hObject2 = MyClassHandle( object2 ); }
For whatever reason, some debuggers are unable to understand handles. Thus while
handle->inspect();
(gdb) call handle.obj->inspect()
main
routine to check for memory leaks. int main() { { // enclosed block // Code that may allocate reference-counted objects } // end enclosed block assert( 0 == IotrRefCount::instances ); return 0; }
The reference count of objects may be managed by calling ReferenceCounting and ReferenceCounting on pointer variables. We advise against maintaining reference counts using pointers because
IotrRelease
on reference counted objects as you are to forget to call delete
on non reference-counted objects. Thus there is no advantage in preventing memory leaks.