Next Chapter Return to Table of Contents Previous Chapter

CHAPTER 5: HETEROGENOUS ARRAYS

In Chapter 3, we introduced the use of heterogenous arrays to allow a container to store different types of objects. In that chapter, we mentioned ways that arrays could be used as heterogenous containers. This chapter will investigate some of these ways more thoroughly. In particular, we'll look at ways to implement reference counting, a technique used to handle the problems of pointer aliasing. But first, we'll review some of the ideas we presented in Chapter 3.

ARRAYS OF POINTERS

By definition, an array can only easily store one type of object, since each element must be the same size. So how do we create heterogenous arrays, where the elements are different object types--perhaps of different sizes? The answer is that we can't. We can, however, store pointers to the objects. That's because pointers are usually the same size, regardless of the type of object they point to. Figure 5.1 shows an example of a heterogenous array that points to shape objects.

Arrays Pointing to Any Type of Object

The Array classes discussed in Chapter 4 can be readily used to create heterogenous arrays. In the most general case, the element type should be void *, which allows any type of object to be pointed to. However, this leads to all sorts of problems, since we can use the objects only if the pointers are typecast, an error-prone activity. For example:

Figure 5.1 A heterogenous array.

Darray<void *> vpa(2)  // Create an array of void pointers

vpa[0] = new Toaster; // Store a Toaster object

vpa[1] = new Ufo;     // Store a Ufo object

((Ufo *)vpa[1])->Fly();            // Okay

((Ufo *)vpa[0])->Fly();            // Likely program crash!

Arrays Pointing to Objects from a Hierarchy

One way to reduce typecasting problems like those shown in the previous section is to restrict the objects to be only those from a particular class hierarchy. Then, good use of virtual functions will reduce (but not eliminate) the need for typecasting.

For example, the following code shows a class hierarchy that consists of a base Shape class and a derived Circle class. A virtual Area( ) function is defined in each class. For Shape objects, Area( ) returns 0. For Circle objects, the standard r2 formula is used. We can store both types of objects in an array consisting of Shape pointers:

class Shape {

protected:

float x, y;

public:

Shape()                    { x = 0; y = 0; }

Shape(float xx, float yy)  { x = xx; y = yy; }

virtual float Area()       { return 0; }

};

class Circle : public Shape  {

protected:

float r;

public:

Circle() { r = 0; }

Circle(float xx, float yy, float rr) : Shape(xx,yy) { r = rr; }

virtual float Area() { return 3.14*r*r; }

};

Darray<Shape *> spa(2);  // Create array of shape pointers

spa[0] = new Shape(1,2);

spa[1] = new Circle(3, 4, 5);

for(int i = 0; i<2; i++) cout << spa[i]->Area() << '\n';

Pointer Aliasing

On the surface, the use of arrays of pointers to implement heterogenous arrays seems quite easy. However, one major problem lurks beneath the surface: pointer aliasing. Both examples that we've shown neglected to do one important thing: delete the objects that were created on the heap. This isn't terribly difficult to do in simple cases. For instance, we could add the following two lines to our previous example:

delete spa[0]; // Delete object being pointed to

delete spa[1];

However, what happens if we were to have two arrays in our example, and we decide to copy one array into the other?

Darray<Shape *> spb(2);

spb = spa; // What happens here?

Due to the design of the Array classes, only the pointers to the Shape objects would be copied, not the objects themselves. The result is that both arrays would point to the same objects. Now the question is: which array should be responsible for deleting the actual objects? Or should we make copies of the objects instead?

We can use four basic methods--ranging from simple to complex--to handle this aliasing problem:

1. Use the convention that heterogenous arrays are never responsible for cleaning up the objects they point to. Instead, whatever created the objects is responsible. This is the default behavior when using one of the Array classes with pointer elements.

2. A flag could be stored in each array to indicate whether the array is responsible for cleaning up the objects or whether some other array is responsible. Only the first array that references a particular set of objects will have this flag set to one. For all other array copies, the flag is set to zero.

3. Store a flag in each object, indicating whether the object itself or the array it's associated with is responsible for cleanup of the object. In this scenario, a flag is still needed in each array, (as in method 2), in case the object's flag says it's some array's responsibility, and in case multiple array copies exist. We have to know which array is responsible for the objects.

4. Extend the idea of method 3 and, instead of using a boolean flag, use a counter to count how many references are made to the object. When this count goes to zero, the object should be destroyed. With this method, no flags are needed in the arrays themselves.

Method 4 is the reference counting scheme mentioned in Chapter 3. In that chapter, we didn't actually implement reference counting, so we'll do this next.

REFERENCE COUNTING

Reference counting works by storing a counter with each object. Each time the object is referenced by another entity (such as an array or perhaps some other object), the counter is incremented. When an entity is finished with the object, the counter is decremented. When the counter goes to zero, it means no entities are referencing the object, so it can be deleted. Thus, one way to safely implement heterogenous arrays is to point only to objects that are reference counted.

You can implement refererence counting in two basic ways:

1. Embed a reference count directly into each object. This works if you haven't defined your classes yet or don't mind modifying existing classes.

2. Indirectly associate each object with a reference count. This method is for those cases where you don't want to (or can't) modify your existing classes.

We'll investigate both of these methods next.

REFERENCE-COUNTED OBJECTS

The simplest and most efficient way to implement reference counting is to directly embed a reference count into each object. This suggests a single-rooted hierarchy, where every object type is derived from a base class that sets up the reference counting. For example, we can define a CountedObj class for this purpose:

class CountedObj {

protected:

unsigned refcnt;

public:

CountedObj()           { refcnt = 1; }

virtual ~CountedObj()  { }

void IncRef()          { refcnt++; }

void DecRef()          { refcnt-; }

int UnReferenced()     { return refcnt == 0; }

};

From this class, we derive all the other types of objects we wish to have reference counted. In the case of the existing Shape and Circle classes given earlier, one simple modification will suffice--merely derive Shape from CountedObj:

class Shape : public CountedObj {

...

};

Now Shape and Circle objects have embedded reference counts. However, we have yet to show how these reference counts get manipulated. That's the responsibility of whatever uses the objects. In the case of heterogenous arrays, where pointers to objects are used, the responsibility falls on the pointers.

SMART POINTERS

To handle pointers to reference-counted objects, we'll define a new type of pointer called smart pointers. These work much like normal pointers, except they keep track of the number of references to the objects they point to. The following SmartPtr class implements smart pointers as a concrete data type.

template<class TYPE>

class SmartPtr {

protected:

TYPE *objptr; // Actual pointer to object

void Bind(const SmartPtr<TYPE> &p);

void Unbind();

void NewBinding(const SmartPtr<TYPE> &p);

public:

static TYPE null_obj;

SmartPtr(TYPE *p=0);

SmartPtr<const SmartPtr<TYPE> &p);

SmartPtr<TYPE> &operator=(const SmartPtr<TYPE> &p);

~SmartPtr();

TYPE &operator*() const;

TYPE *operator->() const;

};

Complete code for the SmartPtr class is given in the file smartptr.h. (The class is completely inlined.)

Each smart pointer has one data member, objptr, that points to the object being referenced. Any type of object can be used as long as it is reference-counted and has the member functions IncRef( ), DecRef( ), and UnReferenced( ). The reference counter should be set to one by the type's constructor. As you might have suspected, any object type derived from the CountedObj class fits these requirements.

Constructing Smart Pointers

Binding Smart Pointers

Using Smart Pointers Like Ordinary Pointers

Arrays of Smart Pointers

Constructing Smart Pointers

To set up a smart pointer to an object, you must first allocate and initialize the object on the heap (with the reference count set to one), and then pass a pointer to the object in a call to the SmartPtr constructor. For example:

SmartPtr<Shape> ps(new Shape(1,1));

SmartPtr<Shape> pc(new Circle(2,3,4));

In the second constructor call above, pc can point to a Circle object--even though pc is typed to point to a Shape object--since Circle was derived from Shape. This important feature lends itself quite nicely to the creation of heterogenous arrays. For example, we can define a heterogenous array of SmartPtr<Shape> objects that point to both Shape and Circle objects. For simplicity, we use a built-in array:

SmartPtr<Shape> arr[2] = { new Shape(1, 2), new Circle(3, 4, 5) };

Figure 5.2 shows the memory layout of the array we've just constructed. Note that, when building the array, two implicit calls are made to the SmartPtr constructor. We'll look at that constructor now:

template<class TYPE>

SmartPtr<TYPE>::SmartPtr(TYPE *p)

{

if (p) {        // Do we have an object to point to?

objptr = p;   // Point to the object

}

else {          // No. Then point to the null object.

objptr = &null_obj;

objptr->IncRef();  // Must account for reference

}

}

The constructor works as follows: if the parameter p isn't a null pointer, we initialize objptr to reference the object pointed to by p. However, if the allocation of the object fails, p will be null. Rather than have objptr also be null, we point to the special static object null_obj, set aside for just this purpose. By using null_obj, we can guarantee that objptr will never be null. This greatly simplifies the rest of the SmartPtr code. If we do reference null_obj, we must increment its reference count, as shown in the constructor. If p isn't null, it's assumed that the reference count for the object pointed to by p is already set, and that this SmartPtr is currently the only reference to it.

The constructor can be used as a default constructor because p defaults to zero. By using the default constructor, you can have a smart pointer automatically set up to point to null_obj. This is useful in declaring an array without explicitly constructing each element. For example:

// Default SmartPtr constructor called 10 times below

Darray< SmartPtr<Shape> > myarr(10);

Notice how we're using a nested template declaration here, declaring a Darray object of SmartPtr<Shape> elements. In this example, each element of myarr will initially point to null_obj.

The static null_obj object must be allocated once somewhere in our program. Also note that we must have one of these objects for each type of smart pointer. For example, if we're going to use smart pointers to point to Shape objects and, let's say, Toaster objects, we would write:

// Allocate null objects for each type of smart pointer

Shape SmartPtr<Shape>::null_obj;

Toaster SmartPtr<Toaster>::null_obj;

Figure 5.2 A heterogenous array of smart Shape pointers.

Binding Smart Pointers

A smart pointer can be bound to an object by pointing objptr to it and then adjusting the reference count for the object. As you've seen, the constructors do this for us initially. But afterwards, we can bind to another object by using the following three routines:

template<class TYPE>

void SmartPtr<TYPE>::Bind(const SmartPtr<TYPE> &s)

// Assumes the object is not already bound

{

objptr = s.objptr;

objptr->IncRef();

}

template<class TYPE>

void SmartPtr<TYPE>::Unbind()

{

objptr->DecRef();

if (objptr->UnReferenced()) delete obj;

}

template<class TYPE>

void SmartPtr<TYPE>::NewBinding(const SmartPtr<TYPE> &s)

{

if (objptr != s.objptr) { // Prevents accidental deletion

Unbind() ;

Bind(s);

}

}

The Bind( ) routine sets objptr to point to the same object that some other smart pointer is using. Aliasing occurs at this point, which is recorded by incrementing the reference count for the object. The Unbind( ) routine does the reverse by decrementing the reference count. If the reference count goes to zero, the object is de-allocated by calling delete. The NewBinding( ) routine combines the actions of Bind( ) and Unbind( ) by first unbinding from the current object and then binding to a new one. Figure 5.3 shows an example of this process.

Figure 5.3 Binding to a new object.

The copy constructor, overloaded assignment, and destructor use the binding routines:

template<class TYPE>

SmartPtr<TYPE>::SmartPtr(const SmartPtr<TYPE> &p)

{

Bind(p);

}

template<class TYPE>

SmartPtr<TYPE> &SmartPtr<TYPE>::operator=(const SmartPtr<TYPE> &p)

{

NewBinding(p);

return *this;

}

template<class TYPE>

SmartPtr<TYPE>::~SmartPtr()

{

Unbind();

}

As is true with any normal pointer, copying one smart pointer into another causes only the pointer objptr to be copied, not the object being pointed to. However, smart pointers, unlike normal pointers, also keep track of the number of references made to the object. In the case of the copy constructor, a single call to Bind( ) does the trick. But for assignments, we must first unbind from the object currently being pointed to, and then bind to the new one. The NewBinding( ) routine does the work here.

When a smart pointer is destroyed, a call to Unbind( ) is made to decrement the reference count for the object being referenced. The object itself won't be deleted unless the reference count goes to zero.

Using Smart Pointers Like Ordinary Pointers

In Chapter 3, we espoused the virtues of using concrete data types. The idea is to be able to use a user-defined type as though it were built into the language. It would be convenient if we could use smart pointers like any other pointer. For example, we should be able to de-reference them as though they were ordinary pointers. We can do this by overloading the two operators '*' and '->':

template<class TYPE>

const TYPE &SmartPtr<TYPE>::operator*() const

{

return *objptr;

}

template<class TYPE>

TYPE *SmartPtr<TYPE>::operator->() const

{

return objptr;

}

When combined, these operators make a SmartPtr<TYPE> object appear to be a TYPE * object. For example, we can make a smart Shape pointer look like a Shape pointer, as in:

SmartPtr<Shape> sp = new Shape(1, 2);

// Two calls to operator'->'

sp->x = 42;

cout << "Area: " << sp->Area() << '\n';

Shape s = *sp; // Call to operator'*'

*sp = s;       // Illegal!

The last statement tries to do a wholesale assignment to a de-referenced Shape object via the operator*( ) function. This shouldn't be allowed because the embedded reference count will get set as well--greatly messing up the scheme of things. To disallow such dangerous statements, operator*( ) returns a reference to a constant TYPE, not just a TYPE. This means the object can only be read, not written to.

We didn't use the same const modifier on the operator->( ) function, however. There ought to be some way to change the underlying object's data! Unfortunately, this creates a loophole because, even though refcnt is protected, the functions IncRef( ) and DecRef( ) aren't. This means we can write corrupting code like:

sp->DecRef(); // Not cool!

Surprisingly, it can be difficult to fix this loophole. We would like to make the functions DecRef( ) and IncRef( ) private to only the classes that need access to them, such as the appropriate SmartPtr class. We could do this by declaring refcnt protected or private in the CountedObj class, and then declare SmartPtr as a friend. The problem is that SmartPtr is a template, and we can't specify all instantiations of a template as friends of a particular class. We must spell out the instantiations one at a time. For example:

class Shape;    // Forward class declarations

class Widget;

template<class TYPE> class SmartPtr; // Forward template declaration

class CountedObj {

private:

friend class SmartPtr<Shape>;

friend class SmartPtr<Widget>;

... // All other friends go here

unsigned refcnt;

public:

...

};

We've left the loophole in so that we don't have to go to all this trouble. So be careful!

To make smart pointers look even more like ordinary pointers, you might also want to overload the comparison operators '==' and '!='. We didn't do that in the SmartPtr class, but here's how operator==( ) and operator!=( ) might be defined as friend functions:

template<class TYPE>

int operator==(const SmartPtr<TYPE> &a, const SmartPtr<TYPE> &b)

{

return a.objptr == b.objptr;

}

template<class TYPE>

int operator!=(const SmartPtr<TYPE> &a, const SmartPtr<TYPE> &b)

{

return a.objptr != b.objptr;

}

Finally, you might also want to conveniently test whether a SmartPtr is "null" by overloading the '!' operator and providing an int type conversion operator. Here's how:

template<class TYPE>

int SmartPtr<TYPE>::operator!()

// Returns 1 if pointer references null_obj; otherwise, returns 0

{

return objptr == &null_obj;

}

template<class TYPE>

SmartPtr<TYPE>::operator int()

// Returns 1 if pointer doesn't reference null_obj; otherwise, returns 0

{

return rep != &null_obj;

}

...

SmartPtr<Shape> p(new Shape(1, 2));

if (!p) cout << "Allocation failed\n";

if (p) cout << "Allocation succeeded\n";

Arrays of Smart Pointers

We'll now take a closer look at smart pointers by using them with the user-defined Array classes created in Chapter 4. Here is a dynamic array of smart shape pointers:

typedef SmartPtr<Shape> SmartShapePtr;

// Declare a dynamic array of smart shape pointers

Darray<SmartShapePtr> arr(2);

With such a declaration, the two smart shape pointers stored in the array will reference the null shape object. We could assign them to new objects, as follows:

arr[0] = new Shape(42, 25);

arr[1] = new Circle(55, 17, 3);

Note that, when the assignments take place, the array elements are unbound from the null shape object, and then bound to the new objects just created.

The array elements can be used just as though they were pointers to Shape objects, as in:

for (int i = 0; i<2; i++) cout << arr[0]->Area() << '\n';

Since Area( ) is virtual, and we are using base class Shape pointers, the appropriate area of each shape is printed.

When you use an array of smart pointers, rest assured that the constructors and destructors for all referenced objects are called the correct number of times, even if the array is a variable-length, resizable array. This is true because both the Array and SmartPtr classes are designed to carefully handle construction and destruction of array element objects.

For example, suppose we define a static variable-length array of Shape pointers, and then add some elements. As a twist, we'll make the second and third elements point to the same Shape object. To understand what's going on, remember that all elements of a variable-length array start out unconstructed.

SVarray<SmartShapePtr, 5> shapes; // 5 unconstructed elements

// Construct the first two elements

shapes.Add(new Shape(1,1));

shapes.Add(new Circle(0, 0, 4));

// Create a third element bound to null shape object

shapes.Add(0);

// Now, bind it to same shape that the second element references

shapes[2] =  shapes[1];

Figure 5.4 shows the memory layout of the resulting array. As you can see from the figure, when one element is assigned to another, only the pointers are assigned--not the objects being pointed to. Since the overloaded SmartPtr<Shape> assignment operator is in use, the proper binding and unbinding takes place. For the same reason, copying between smart pointer arrays also works properly. This copying is very efficient, especially when the referenced objects are large.

Figure 5.4 Memory layout of a smart pointer array.

What is the cost of using smart pointers rather than ordinary pointers in an array? In both cases, heap space will be allocated for the target object of each array element. When smart pointers are used, each target object will also have one extra integer to store the reference count. Hence, that is the net overhead for using smart pointers over ordinary pointers. Normally, this overhead won't be noticeable, but it is there.

At this point, you might be concerned about the amount of code space required for the smart pointer class templates themselves. Note that the member functions are all very simple and can easily be inlined. The amount of code added by using smart pointers, while not zero, won't be too great for most applications.

Two test programs tstsp.cpp and tstsp2.cpp are provided on the disk to illustrate how smart pointers are used in both fixed-length and resizable heterogenous arrays of shapes.

INDIRECT SMART POINTERS

The smart pointer technique that we've just shown works great--as long as you can directly embed a reference count into each object. But suppose you have an existing class that you want reference counted, and don't want to modify the class? One way to add the reference counting is by using indirect smart pointers.

Indirect smart pointers are like ordinary smart pointers except that one extra level of indirection is required to get to the object. Between the pointer and the target object is another object, which stores a reference count and a pointer to the target object. This intermediate object serves to make the target object look like a reference-counted object. Figure 5.5 illustrates the proposed setup.

Figure 5.5 Sample memory layout for indirect smart pointers.

Here is a class that can serve as the intermediate object type:

template<class TYPE> class iSmartPtr; // Forward template declaration

template<class TYPE>

class ObjRep {

// Assumes the object pointed to is stored on heap,

// except for null_obj

private:

friend class iSmartPtr<TYPE>;

unsigned refcnt;

TYPE *objptr;

ObjRep();

void IncRef();

void DecRef();

ObjRep(TYPE *p);

static TYPE null_obj;

};

template<class TYPE>

ObjRep<TYPE>::ObjRep()

// To be used only once to create a null ObjRep object

{

objptr = &null_obj;

refcnt = 1;

}

template<class TYPE>

ObjRep<TYPE>::ObjRep(TYPE *p)

{

objptr = p;

refcnt = 1;

}

template<class TYPE>

void ObjRep<TYPE>::IncRef()

{

refcnt++;

}

template<class TYPE>

void ObjRep<TYPE>::DecRef()

{

refcnt-;

if (refcnt == 0) delete objptr;

}

The name ObjRep stands for object representation. The class represents an ordinary object as though it were a counted object. You'll notice that ObjRep is very similar to the CountedObj class. The main difference is that the target object is not stored in the ObjRep object, but rather is pointed to by objptr. The parameterized constructor sets up objptr and also initializes the reference count. The DecRef( ) function is responsible for deleting the target object when the reference count goes to zero. Note that the CountedObj class does not delete the object, since it is the object! In this case, the deletion is left as a task for the iSmartPtr class.

Notice that we've declared a static member, null_obj. This is the target object to point to when we want a null ObjRep object. We do this for the same reason it was done in the SmartPtr class--to guarantee that we never have a real null pointer.

Having defined the ObjRep class, we can now show the iSmartPtr class, which is used to implement indirect smart pointers. Note the lowercase i in the prefix:

template<class TYPE>

class iSmartPtr {  // Indirect smart pointers

protected:

static ObjRep<TYPE> null_rep;

ObjRep<TYPE> *rep;

void Bind(const iSmartPtr<TYPE> &p);

void Unbind();

void NewBinding(const iSmartPtr<TYPE> &p);

public:

iSmartPtr(TYPE *p=0);

iSmartPtr<TYPE> &operator=(const iSmartPtr<TYPE> &p);

~iSmartPtr();

TYPE &operator*() const;

// Will only compile if TYPE is a structure

TYPE *operator->() const;

};

Complete code for the ObjRep and iSmartPtr classes is given in the file ismartptr.h. (The classes are completely inlined.)

The two classes ObjRep and iSmartPtr provide an example of cooperating classes. The ObjRep class gives the low-level data representation for reference-counted objects, while the iSmartPtr class provides the high-level interface. This technique of arranging two classes, one being a high-level wrapper around a low-level representation, is sometimes referred to as the letter-envelope idiom. See [Coplien 92]. Here, the iSmartPtr class is the "envelope" that wraps around the "letter" class ObjRep.

The iSmartPtr class is very similar to the SmartPtr class. The main difference is that it points to an ObjRep<TYPE> object rather than directly to a TYPE object. A static ObjRep object, null_rep, is declared (to be used in lieu of a null pointer). This is in addition to the null static TYPE object declared in the ObjRep class, which is in fact referenced by null_rep. Both of these objects must be allocated once somewhere in your program--one pair per type of indirect smart pointer. The following statements show how to do this for indirect smart Shape pointers:

// Allocate a null Shape object to be referenced by

// a null ObjRep object, also allocated

Shape ObjRep<Shape>::null_obj;

ObjRep<Shape> iSmartPtr<Shape>::null_rep;

The iSmartPtr functions are virtually the same as their SmartPtr counterparts, and, as such, many will not be shown here. The functions not shown can be defined by taking the corresponding SmartPtr functions and replacing objptr with rep. Here are the functions that require changes other than this:

template<class TYPE>

void iSmartPtr<TYPE>::Unbind()

{

rep->DecRef(); // ObjRep handles any object deletions

}

template<class TYPE>

iSmartPtr<TYPE>::iSmartPtr(TYPE *p)

}

rep = (p) ? new ObjRep<TYPE>(p) : 0;

if (rep == 0) {

rep = &null_rep;

rep->IncRef();

}

}

template<class TYPE>

TYPE &iSmartPtr<TYPE>::operator*() const

{

return *(rep->objptr);

}

template<class TYPE>

TYPE *iSmartPtr<TYPE>::operator->() const

// Will only compile if TYPE is a structure

{

return rep->objptr;

}

Note how the functions operator*( ) and operator->( ) return a pointer not to the object representation, but rather directly to the target object. Since the target object does not store the reference count, we don't have any unsafe loopholes with these functions. For that reason, operator*( ) returns a TYPE reference, instead of a const TYPE reference as was the case in the SmartPtr class. This allows assignments like the following to take place:

iSmartPtr<Shape> sp(new Shape(1,1));

Shape s(42, 17);

*sp = s; // Update the referenced shape object with a new shape

One potential problem with the operator->( ) function: it will only compile if TYPE is a structure. If TYPE isn't a structure, you'll have to comment the function out. (Too bad there's no way to tell at compile time whether a type is a structure!) Note that the operator->( ) function for SmartPtr doesn't have this problem, since in this case TYPE will always be a structure.

Arrays of Indirect Smart Pointers

Arrays of Indirect Smart Pointers

We'll now show what an array of indirect smart pointers looks like. The following array is the same as one we gave earlier for direct smart pointers, except now we use indirect smart pointers:

typedef iSmartPtr<Shape> SmartShapePtr;

SVarray<SmartShapePtr, 5> shapes; // 5 unconstructed elements

// Construct the first two elements

shapes.Add(new Shape(1, 1));

shapes.Add(new Circle(0, 0, 4));

// Create a third element bound to the null ObjRep<Shape> object

shapes.Add(0);

// Now, bind it to same shape that the second element references

Shapes[2] = shapes[1];

Figure 5.6 shows the resulting memory layout of the indirect smart pointer array. Compared to the use of ordinary pointers (or even to the use of direct smart pointers), indirect smart pointers cost more overhead. An extra heap allocation is needed for the intermediate ObjRep object, which stores the reference count and target object pointer. Note that, like the SmartPtr class, the iSmartPtr class and its associated ObjRep class can be completely inlined, so very little overhead occurs in creating the class itself.

Figure 5.6 Memory layout of a variable-length indirect smart pointer array.

Whether the overhead that results from using indirect pointers becomes a problem depends on how big the target objects are initially, and how many of them you try to store. In the case of large arrays of reference-counted integers, the overhead would be significant. If you need aliasing protection, try to use direct smart pointers if you can, since their overhead is much smaller.

Test programs for indirect smart pointers are given on disk in the files tstisp.cpp and tstisp2.cpp. These programs test both fixed-length and resizable arrays of indirect smart pointers.

SMART POINTER SAFETY ISSUES

The SmartPtr class has a loophole where the reference count can be manipulated directly by using the operator->( ) function. Unfortunately, there is also another loophole, (which exists in the iSmartPtr class as well). When either a direct or indirect smart pointer object is constructed, a pointer to the target object to be referenced is passed. Although the target object is supposed to be allocated on the heap, nothing prevents you from passing the address of an object allocated statically. For instance, you might accidentally try:

Shape s(1, 1):

Shape> sp(&s); // Not a good idea!

Things will go haywire when the reference count for sp goes to zero because an attempt will be made to free the statically allocated object from the heap. A program crash will probably result. Note that both the SmartPtr and iSmartPtr classes violate this rule with their static null target objects. However, these objects are defined private to the classes, and can't otherwise be manipulated. Their reference counts should never go to zero--as long as everything is working correctly.

Another problem is the possibility of passing the address of the same target object to two different smart pointers, creating multiple reference counts for the object. Again, confusion will result. Here's an example of something you should never do:

Shape *p = new Shape(1, 1);

SmartPtr<Shape> sp1(p); // Okay

SmartPtr<Shape> sp2(p); // Not!

Keep in mind that the iSmartPtr class has these same problems.

DERIVED SMART POINTERS

You can help prevent duplicate reference counts from being accidentally created for an object. The idea is to define a smart pointer class that handles the construction of the referenced object itself. For example, we could create a SmartShapePtr class, derived from SmartPtr, to work directly with Shape objects, as follows:

class SmartShapePtr : public SmartPtr<Shape> {

public:

SmartShapePtr(int x, int y);

};

SmartShapePtr::SmartShapePtr(int x, int y)

: SmartPtr<Shape>(new Shape(x, y))

{

// Nothing else to do

}

...

SmartShapePtr(0, 1); // Much higher level than before

Now there's no possibility for accidental duplicate reference counts. A SmartCirclePtr class could be coded similarly:

class SmartCirclePtr : public SmartPtr<Shape> {

public:

SmartCirclePtr(int x, int y);

} ;

SmartCirclePtr::SmartCirclePtr(int x, int y, int r)

// Note that we use the SmartPtr<Shape> class below, rather

// than SmartPtr<Circle> to save some template generation

: SmartPtr<Shape>(new Circle(x, y, r))

{

// Nothing else to do

}

Note that this same technique can also be used for the indirect smart pointer class iSmartPtr.

With this technique, we must construct, by hand, a smart pointer class for every type of object we wish to point to. This can quickly get tedious. We need constructors with different parameters for each object that we want to point to, but templates are not flexible enough to allow us to specify the various types of constructors. So, unless we restrict the types being used to always have constructors with similar arguments, we're stuck.

The result is this: if you want absolute safety, you must code separate smart pointer classes by hand. If you don't need absolute safety, or don't want to pay for it, you can use the SmartPtr and iSmartPtr classes by themselves.

Go to Chapter 6 Return to Table of Contents