Recently, we've looked at some of the benefits of using smart pointers in
C++ programs ("Automatically Deallocating
Memory Using Smart Pointers" [September 1994] and "Creating
a Smart Pointer Template Class" [October 1994]). As we've
seen, smart pointers simplify many aspects of managing heap objects.
However, the smart pointers we've examined so far assume
that there's only one smart pointer for a given heap object.
If you initialize two simple smart pointers (like the ones we've
shown) with one heap object, the first smart pointer will delete
the heap object when you destroy it. From that point, the second
smart pointer won't address a valid object, and it will
then try to delete the heap object again from the second smart
pointer's destructor (assuming the program hasn't
crashed by then).
In this article, we'll show how you can avoid this problem
by implementing reference counting in your smart pointer
classes. Reference counting is a technique in which a smart pointer
class checks to see if it's the last smart pointer referencing
a heap object before it deletes the heap object.
Before we further describe reference counting, let's briefly
define smart pointer. A smart pointer is a stack-based
object that controls your interaction with a heap-based object.
To create a smart pointer, you create a class that definesfor
stack-based objectsthe operations you'd normally
perform via a pointer.
A smart pointer that implements reference counting has three major
tasks to perform: incrementing the reference count when you initialize
a new smart pointer object, decrementing the reference count when
you destroy a smart pointer object, and deleting the heap object
when the reference count reaches zero. (The reference count will
be zero when the last smart pointer for the heap object leaves
the current scope.)
Each of these three tasks requires access to the reference count
for a heap object. However, you'll need to decide where
you're going to store the reference count. If you decide
to store the reference count with the heap object, you have three
options: create a template "wrapper" class for the
heap objects, hide the count on the heap with a custom operator
new( ) function, or create a base class and derive your
heap object classes from it. For a full description of these techniques,
their benefits, and their drawbacks, BCJ - Other ways to perform reference counting.
If you'd rather keep the reference count for a given heap
object in the smart pointer, you'll need to do a little
more work. (This is because the reference count needs to be accurate,
as the program creates and destroys smart pointers.)
If you want to make the class useful for real programs, you'll
want your reference- counting scheme to accommodate smart pointers
for multiple heap objects. To maintain the reference counts this
way, you'll need to create two static arrays for the smart
pointer class.
The first array will contain pointers to the heap objects that
the smart pointers manage, and the second array will contain the
corresponding reference counts for the pointers in the first array,
as illustrated in Figure A. If you implement these arrays using
Borland's template class TArrayAsVector, you'll
be able to simplify the process of adding or deleting a new heap
object/reference count combination to the arrays.
The most significant advantage to keeping the reference counts
in the smart pointer class is that you can use pointers to existing
class objects. Therefore, you don't need to wrap the objects
in a template class, provide a custom operator new( )
function, or create a special class for reference-counted objects.
Now, let's create a simple example program that implements
reference counting by storing the reference counts in the smart
pointer class. To begin, launch the Borland C++ 4.0 Integrated
Development Environment (IDE).
When the IDE's main window appears, choose New Project...
from the Project menu. When the New Project dialog box appears,
enter \REFCNTR\REFCNTR.IDE in the Project Path And Name
entry field, select EasyWin in the Target Type list box, select
Small in the Target Model combo box, and then click the Class
Library check box in the Standard Libraries section.
Next, click Advanced... to display the Advanced Options
dialog box. Select the CPP Node radio button, select the DEF check
box, and then click OK. When the New Project dialog box reappears,
click OK to create the project.
When the IDE finishes creating the project, the REFCNTR.IDE project
window will appear. In this window, right-click on the name refcntr
[.def] and choose Edit Node Attributes... from the
pop-up menu. In the Node Attributes dialog box, enter \BC4\LIB\DEFAULT.DEF
in the Name entry field and click OK.
Now, choose New from the File menu. When the new editor window
appears, enter the code from Listing A.
Listing A: smartptr.h
When you finish entering the code, choose Save As...
from the File menu. In the Save File As dialog box, enter \SMARTPTR.H
in the File Name entry field and click OK.
Double-click on the refcntr [.cpp] node in the project
window. When the editor window for this file appears, enter the
code from Listing B. When you finish entering this code, choose
Save from the File menu.
Listing B - REFCNTR.CPP
Finally, right-click on refcntr [.exe] and choose Edit
Node Attributes... from the pop-up menu. In the Node
Attributes dialog box, choose Diagnostics from the Style Sheet
combo box, and then click OK.
Choose Event Log from the View menu to display the Event Log window.
(You'll be able to view the TRACE messages in
this window.) Then, choose Run from the Debug menu to build and
run the program.
By the way, the first time you run this program, you may see a
Cannot Write to AUX Device error message. If so, click
Cancel and ignore the message.
When the program runs, it won't display the standard EasyWin
screen because the program didn't create any direct output.
Instead, you'll see a list of messages in the Event Log
window. Now, let's step through the code for this program
to see how it produces this output.
At the center of this reference-counting scheme are two arrays
that are static members of the smartPtr class: refCountArray
and pointerArray. The refCountArray member
is a dynamically sized TArrayAsVector object that holds
the actual reference counts as long int values. The pointerArray
member is a similar TArrayAsVector object that holds
the heap object pointers.
When you create a smartPtr object, the constructor calls
the private AddRef( ) static member function. This
function searches the array of heap pointers to see if the newPtr
pointer is already there. If the pointer exists in the pointer
array, the function simply increments the reference-count value
that corresponds to the newPtr pointer.
If the newPtr pointer isn't in the pointer array,
the AddRef( ) function displays a registration message
with the TRACE( ) diagnostic macro. Then, the AddRef( )
function adds the pointer to the pointer array and adds a reference
count of 1 to the refCountArray member.
When the smartPtr object's destructor runs, it
calls the private DelRef( ) static member function.
The DelRef( ) function determines the position of
the object's pointer and then decrements the corresponding
reference count. If the count reaches zero, the function displays
a deletion message with the TRACE( ) diagnostic
macro, and then it destroys the heap object and the reference-count
element for that object.
The remainder of the class is fairly easy to understand. However,
at the end of the SMARTPTR.H file, you'll notice that we've
declared the INIT_SMART_PTR_STATICS macro to initialize
the two static arrays and a char pointer that addresses
the type name of the heap objects the smart pointers manage. If
you use the smartPtr class, you'll need to include
a call to this macro for each smart pointer type.
Now let's quickly review how the REFCNTR program uses the
SmartPtr class. As you look through the source code,
you'll be able to trace the program's execution
by referring to Figure B, which is the output that
appears in the Event Log window.
Figure B - When you run the REFCNTR.EXE program, you'll see these TRACE messages in the Event Log window.
At the beginning of the source file REFCNTR.CPP, we declare the
Money struct. There's nothing special about this struct
in relation to our SmartPtr classwe just created
it to show how the SmartPtr class can work with any type
of object that you dynamically allocate.
Immediately below the Money declaration, you'll
find the line
We call this macro to initialize the static arrays in the SmartPtr
class. Since we're passing the Money class name
to the macro, the macro is able to set the static variables for
the SmartPtr<Money> template class.
Below the line that calls the macro, you'll find a typedef
statement that allows us to use the name SmartMoneyPtr
instead of SmartPtr<Money>. (This makes the code
easier to read.)
For now, skip the foo( ) function and examine the
main( ) function instead. The first statement in
the main( ) function is a call to the printSummary( )
function from the SmartMoneyPtr class. (Remember, this
is really the SmartPtr<Money> template class.)
Since the printSummary( ) function is a static function,
you can call it using the name of the class instead of calling
it using the SmartMoneyPtr object's identifier.
The purpose of this function is simply to use the TRACE diagnostic
macro to display the heap objects and reference counts that the
class is currently storing in its static arrays. As the program
runs, we call this function repeatedly to allow you to see how
the contents of the arrays change as the program creates and destroys
smart pointer objects.
In the remainder of the program, we create SmartMoneyPtr
objects in various ways to demonstrate the flexibility you'll
have when you maintain the reference counts in the smart pointer
class. (Reference-counting schemes that store the count with the
heap object won't have this kind of flexibility.)
If you look at the foo( ) function, you'll
see a particularly difficult scenario. Near the end of the main( )
function, we pass the twoBitObject variable to the foo( )
function. Since the function expects a Money* parameter,
the compiler will call the SmartMoneyPtr class's
conversion operator to convert the smart pointer to a Money*
value.
Inside the foo( ) function, we create a new SmartMoneyPtr
object named fooMoney and initialize it with the Money*
function parameter newMoney. Then, we create another
SmartMoneyPtr (initializing it with a new heap object)
and abandon that heap object by assigning the fooMoney
smart pointer to the tempMoney smart pointer. (If we
weren't using smart pointers, this would create a definite
memory leak.)
As you refer to Figure B, notice that the SmartMoneyPtr
class deletes each heap object when there's no longer a
smart pointer referring to that heap object. This is proof that
our reference-counting technique works.
If you use the SmartPtr class in your programs, you'll
want to avoid two potential problems. The first problem can occur
if you create smart pointer classes for related types.
For example, if you declare a class named Base and then
derive a new class named Derived, the compiler will allow
you to write the following code:
Now, the SmartPtr<Derived> class's static
array of heap objects will contain the address of the derivedObject
heap object and a reference count of 1. Unfortunately, the SmartPtr<Base>
class's static array will also contain this object's
address and a separate reference count of 1. (This happens because
template classeseven those whose template parameter types
are relatedhave no relationship with each other.)
When the compiler destroys the derivedPtr object, it
will delete the derivedObject heap object because its
reference count will be 0. When the compiler destroys the basePtr
object, it will try to delete the heap object a second time. Unfortunately,
the C++ language doesn't define what happens when you delete
a heap object twice.
To avoid this problem completely, create smart pointers only for
the base classes in your program. If you must create smart pointers
for derived classes (to call derived-class member functions),
don't initialize different types of smart pointers with
the same heap object.
The second problem can occur if you use the SmartPtr
class in a project that has multiple source files. If you create
smart pointer objects in different source files for the same type
of heap object, you may see symbol redefinition errors if you're
using the -Jgd compiler switch. (This generates global
definitions of template class instances.)
Smart pointers can make it easy to manage deallocating the memory
for heap objects. If you use the technique we've shown
here to add reference counting to your smart pointer class, you'll
be able to safely initialize more than one smart pointer to any
individual heap object.
Who's counting?
Figure A - The smart pointer class can count references by maintaining an array of pointers and an array of reference counts.
Reference counter
#if !defined( __CLASSLIB_ARRAYS_H )
#include <\classlib\arrays.h>
#endif
template<class T> class SmartPtr
{ T* ptr;
static TArrayAsVector<long> refCountArray;
static TArrayAsVector<T*> pointerArray;
static const char* className;
static void AddRef(T* newPtr)
{ int ptrIdx;
ptrIdx = pointerArray.Find(newPtr);
if(ptrIdx != INT_MAX)
{ ++(refCountArray[ptrIdx]); }
else
{ TRACE("Registering " \
<< (void*)newPtr);
pointerArray.Add(newPtr);
refCountArray.Add(1);
}
}
static void DelRef(T* currentPtr)
{ int ptrIdx = pointerArray.Find(currentPtr);
if(ptrIdx != INT_MAX)
{ -(refCountArray[ptrIdx]);
if(refCountArray[ptrIdx] < 1)
{ TRACE("Deleting " \
<< (void*)pointerArray[ptrIdx]);
pointerArray.Destroy(ptrIdx);
refCountArray.Destroy(ptrIdx);
}
}
else
TRACE("Couldn't find pointer in array");
}
public:
SmartPtr(T* p) : ptr(p)
{ AddRef(ptr); }
~SmartPtr( )
{ DelRef(ptr); }
operator T*( ) const
{ return ptr; }
operator const T*( ) const
{ return ptr; }
T* operator-> ( )
{ return ptr; }
SmartPtr<T>& operator= (const SmartPtr<T>& source)
{ DelRef(ptr);
ptr = source.operator T*( );
AddRef(ptr);
return *this;
}
SmartPtr(const SmartPtr<T>& source)
{ ptr = source.operator T*( );
AddRef(ptr);
}
static void printSummary( );
};
template <class T>
void SmartPtr<T>::printSummary( )
{
TRACE("** Reference Count Summary **");
int count = pointerArray.GetItemsInContainer( );
for(int ct = 0; ct < count; ++ct)
{
TRACE(" " << className << " object @" \
<< (void *)pointerArray[ct] \
<< " - Ref Count = " << refCountArray[ct]);
}
TRACE("*****************************");
}
#define INIT_SMART_PTR_STATICS(T) \
const char* SmartPtr<T>::className = "<"#T">";\
TArrayAsVector<long> SmartPtr<T>::refCountArray = \
TArrayAsVector<long>(5,0,5);\
TArrayAsVector<T*> SmartPtr<T>::pointerArray = \
TArrayAsVector<T*>(5,0,5);
#include "smartptr.h"
struct Money
{
Money(long d, long c) :
dollars(d), cents(c) {}
long dollars;
long cents;
};
INIT_SMART_PTR_STATICS(Money);
typedef SmartPtr<Money> SmartMoneyPtr;
void foo(Money* newMoney)
{
TRACE("Entering foo( )");
SmartMoneyPtr::printSummary( );
// Create a smart pointer for
// the parameter heap object pointer
SmartMoneyPtr
fooMoney(newMoney);
// Create a new smart pointer
// and heap object
SmartMoneyPtr
tempMoney(new Money(3, 35));
SmartMoneyPtr::printSummary( );
TRACE("Abandon pointer!");
tempMoney = fooMoney;
SmartMoneyPtr::printSummary( );
TRACE("Leaving foo( )");
}
int main( )
{
SmartMoneyPtr::printSummary( );
// Create a heap object & pointer
Money* cash =
new Money(1, 12);
// Create a heap object & smart pointer
SmartMoneyPtr
twoBitObject(new Money(0, 25));
SmartMoneyPtr::printSummary( );
TRACE("Calling Copy Constructor");
SmartMoneyPtr quarter(twoBitObject);
SmartMoneyPtr::printSummary( );
// Create a smart pointer for
// an existing heap object pointer
SmartMoneyPtr
smartCash(cash);
SmartMoneyPtr::printSummary( );
TRACE("Calling operator=");
twoBitObject = smartCash;
SmartMoneyPtr::printSummary( );
// Pass a smart pointer to a function
// that expects a heap pointer
foo(twoBitObject);
SmartMoneyPtr::printSummary( );
return 0;
}
List Test thinger to see if this can be recorded!!!!!
Counter intelligence
A foo and its Money
** Reference Count Summary **
*****************************
Registering 0x3cdc
** Reference Count Summary **
<Money> object @0x3cdc - Ref Count = 1
*****************************
Calling Copy Constructor
** Reference Count Summary **
<Money> object @0x3cdc - Ref Count = 2
*****************************
Registering 0x3cd0
** Reference Count Summary **
<Money> object @0x3cdc - Ref Count = 2
<Money> object @0x3cd0 - Ref Count = 1
*****************************
Calling operator=
** Reference Count Summary **
<Money> object @0x3cdc - Ref Count = 1
<Money> object @0x3cd0 - Ref Count = 2
*****************************
Entering foo( )
** Reference Count Summary **
<Money> object @0x3cdc - Ref Count = 1
<Money> object @0x3cd0 - Ref Count = 2
*****************************
Registering 0x3ce8
** Reference Count Summary **
<Money> object @0x3cdc - Ref Count = 1
<Money> object @0x3cd0 - Ref Count = 3
<Money> object @0x3ce8 - Ref Count = 1
*****************************
Abandon pointer!
Deleting 0x3ce8
** Reference Count Summary **
<Money> object @0x3cdc - Ref Count = 1
<Money> object @0x3cd0 - Ref Count = 4
*****************************
Leaving foo( )
** Reference Count Summary **
<Money> object @0x3cdc - Ref Count = 1
<Money> object @0x3cd0 - Ref Count = 2
*****************************
Deleting 0x3cdc
Deleting 0x3cd0
INIT_SMART_PTR_STATICS(Money);
Check your references
Derived* derivedObject = new Derived( );
SmartPtr<Derived> derivedPtr(derivedObject);
SmartPtr<Base> basePtr(derivedObject);
Conclusion
Copyright (c) 1996 The Cobb Group, a division of Ziff-Davis Publishing Company. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of Ziff-Davis Publishing Company is prohibited. The Cobb Group and The Cobb Group logo are trademarks of Ziff-Davis Publishing Company.