In Automatically deallocating memory using smart pointers from last month's issue of Borland C++ Developer's Journal, we showed how you can create a smart pointer class for a specific data type. In the example, we created a smartCharPtr class. By creating smartCharPtr objects locally in a function (on the stack), we were able to demonstrate how they can automatically delete their corresponding heap objects.
Most of the time, the smart pointer classes you create for different data types will be very similar. Since classes that share common behavior are frequently good candidates for template classes, creating a template class for smart pointers is an obvious next step.
In this article, we'll move the functionality of the smartCharPtr
class to a template class so we can quickly apply this design
technique for other data types. In addition, we'll examine
some other features you may want to add to your smart pointer
classes.
As was the case with the smartCharPtr class from last
month, a smart pointer template class will allow us to create
stack-based objects that control our interaction with a heap-based
object. In the smartCharPtr class, our design goals were
relatively simple: prevent a memory leak via the corresponding
heap object by deleting that object in the smart pointer's
destructor. For any of the simple types, we can use this basic
design with few changes.
To change the smartCharPtr class into a template class,
you'll begin by adding
template <class T> class smartPtr
to the beginning of the class definition and then replacing the smartCharPtr type name with the class template name smartPtr. Then, you'll replace any occurrence of the char type with the template type identifier T.
Finally, you'll need to remove the string-related code from the destructor and the assignment operator (operator =). This includes the destructor code that displays the string (using cout << ptr) and deletes it, and the code in the assignment operator that assumes the ptr member is pointing to an array of characters.
At this point, you'll have a smart pointer template class
that will let you enjoy the memory protection benefits of the
class smartCharPtrbut it will work correctly
with individual objects only. If you wanted to manipulate dynamically
allocated arrays (heap arrays) with a smart pointer class, you'd
need to do some more work.
Since you might want to use your smart pointer template class with types other than char, you'd need to make some adjustments to the smartCharPtr class member functions if you wanted to work with arrays of integers, floating point values, or other types. (This is the case because the strlen() function in the smartCharPtr class's assignment operator expects char pointers.) Unfortunately, creating a smart pointer class for arrays of objects poses some implementation problems.
For example, if you created the class smartArrayPtr, you'd need to make sure you were initializing it with a pointer to an array (since you'd want to call delete [] on the array's pointer). In addition, you'd probably want to overload operator [] to allow you to access individual elements of the array.
If our proposed smartArrayPtr class is beginning to sound
like one of Borland's collection classes, you'll
understand the best reason for not creating a smartArrayPtr
class: Borland's already done so in its BIDS (Borland International
Data Structures) classes. In addition to providing iterator functions
and classes and sanity checking on array indexes (to keep you
from examining data past the end of an array), the BIDS classes
can also serve as very sophisticated smart pointer classes for
arrays (or lists, or hash tables, and so on).
Now, your smartPtr template class works correctly for
individual objects, but using the smart pointers with struct or
class objects is a bit awkward. For example, if you create a new
smart pointer class for an employeeData struct by writing
typedef smartPtr<employeeData> smartEmpPtr;
and you then create a smartEmpPtr object named employee1,
you'd need to write
(*employee1).salary
to access the salary data member of this object.
Instead, you could add an overloaded version of operator ->
to the smartPtr class that would allow you to write
employee1->salary
Unfortunately, now you won't be able to use the smartPtr class for the simple types, since the syntax int number-> is meaningless and will generate a compiler error.
However, since you decided earlier not to create a smartArrayPtr
class (you'll use the BIDS classes instead), and creating
a smart pointer for an individual int, char,
or float value would be overkill, you'll go ahead
and add an overloaded version of operator -> to the
smartPtr class. Now, let's put together all the
pieces of our smart pointer template class.
Launch the Borland C++ 3.1 DOS Integrated Development Environment (IDE). In the IDE, choose New from the File menu and enter the code from Listing A, in the data-file window that appears.
Listing A: SMRTTMPL.CPP
#include <iostream.h> #include <string.h> template<class T> class smartPtr { T* ptr; public: smartPtr(T* p) : ptr(p) {} ~smartPtr() { delete ptr; } operator T*() { return ptr; } operator const T*() { return ptr; } T* operator-> () { return ptr; } smartPtr<T>& operator= (const smartPtr<T>&); // Intentionally undefined - see text smartPtr(const smartPtr<T>&); // Intentionally undefined - see text }; struct employee { employee(char* n, float h) : hourlyRate(h) { strcpy(name, n); } char name[20]; float hourlyRate; employee& operator= (const employee&); }; // A global function that prints data // from an employee object pointer void printEmployeeData(const employee* e) { cout << "The hourly rate for "; cout << e->name << " is $"; cout << e->hourlyRate << endl; } int main() { // Create a normal pointer employee* emp1 = new employee("T. Striker", 3.35); // Create a smart pointer smartPtr<employee> emp2(new employee("B. Shatner", 3.85)); // Display the employee data printEmployeeData(emp1); printEmployeeData(emp2); delete emp1; // Delete the normal pointer return 0; }
Now, choose Save from the File menu and then choose Run from the Run menu. When the IDE finishes building the application, it will shell out to DOS, run SMRTTMPL.EXE, and then return to the IDE.
To view the output of the program, choose Output from the Window
menu. The output from SMRTTMPL.EXE should look like the following:
The hourly rate for T. Striker is $3.35 The hourly rate for B. Shatner is $3.85
As you can tell, the printEmployeeData() function can accept a smart pointer to an employee object as well as a traditional pointer to an employee object.
As we mentioned earlier, adding the overloaded version of operator
-> prevents you from using the smartPtr class
with the simple types. To see this, add the line
smartPtr<int> int1(new int);
anywhere inside the main() function. Now if you rebuild
the project, you'll see the error
operator -> must return a pointer or a class
appear for the body of the operator -> () function.
Remove the line that creates a smartPtr<int> object
to clear the error.
The smart pointer template class we've presented here is
by no means complete. In fact, you'll want to consider
a number of possible refinements to this template class before
implementing it in your programs.
One of the most common uses for smart pointers is detecting the
use of null pointers. If you add assert(ptr); in any
of the smart pointer class's functions that return the
ptr member (and you haven't defined the NDEBUG
constant), your program will halt when you try to use a smart
pointer that's pointing to a 0 address.
In the example from last month's article, there's a potential bug waiting for you. Consider what would happen if you were to create a second smart pointer and initialize it with the first.
By default, a C++ compiler will create a copy constructor and assignment operator for a class if you don't provide them (we haven't). According to the C++ language guidelines, a compiler-generated copy constructor or assignment operator will perform a member-by-member copy from the first object to the second.
At first, this behavior may not seem to pose a threat to your program. However, if your program has two smart pointers whose internal pointers represent the same memory address (and both smart pointers are in the same scope), both smart pointers will try to delete the same block of memory when the program destroys them.
There are a couple of solutions to this. The first solution (and the easiest) is to prevent assignment between two smart pointers or initialization of one smart pointer with another. In the example code, we've done this by declaring overloaded versions of the assignment operator (operator =) and the copy constructor (smartPtr(const smartPtr<T>&)) but never defining them. See Initialization versus assignment for more information. Unfortunately, this is an incomplete solution because it doesn't prevent you from initializing two new smart pointers with the same address.
To solve this problem, you'll need to use the second solution:
reference counting. Reference counting is the act of monitoring
how many smart pointers are using a particular block of memory.
In the destructor of the smart pointer, the smart pointer checks
to see if it's the last one using the memory block; if
it is, it deletes the block. In a future issue, we'll look
at some ways you can implement reference counting.
A smart pointer template class lets you enjoy the benefits of
smart pointers without having to rewrite the class for each new
pointer class. By overloading operator -> in your
smart pointer template class, you can pass smart pointers to many
functions that expect normal pointers, but still take advantage
of a smart pointer's ability to automatically clean up
its heap memory.
Thanks to Cay S. Horstmann of San Jose State University for
inspiring this article.
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.