Robert Mashlan is an independent software consultant doing business as R2M Software Company, specializing in Windows device drivers. He may reached at +1-503-728-0849 or rmashlan@r2m.com on the internet. WWW surfers may see his home page at http://www.csn.net/~rmashlan.
Introduction
One of the complaints about C and C++ is the lack of built-in facilities for checking the valid usage of pointers and arrays. It can be frustrating to track down errors where pointers and array subscripting operations access invalid memory locations. Such errors can cause corruption of the heap, the stack, or static data areas that may not show until much later in the program's execution. With the addition of templates and exceptions to the C++ language specification, however, it is possible to define a template class that throws exceptions for invalid pointer and array usage. I present here two template classes, CheckedPtr and CheckedClassPtr, that do so.Another common problem is a memory leak caused by failing to delete an object when the pointer to it goes out of scope. C++ exceptions introduce a similar problem, because a thrown exception may by-pass the code that deletes a pointer. So I also present the template class HeapPtr, which solves both problems.
Objects of the CheckedPtr class, and its descendant CheckedClassPtr, work like pointers to a typed object. You can use a CheckedPtr object in place of a normal typed pointer in an expression. The template class CheckedPtr defines operators that behave the same way as do the built-in operators for typed pointers.
You use the template class CheckedClassPtr to describe an object that can represent a pointer to an object of some other type. The template class CheckedClassPtr is necessary because of the rules for the member access operator. I discuss this problem later in this article. These template classes support only representations of pointer to object types. They do not support representations of pointers to void, pointers to functions, or pointers to references. The limitation is imposed because pointer arithmetic works only on pointers to objects. For instance, it is an error to use the expression p++ where p is a pointer to void or a function pointer. It is not possible to declare a pointer to a reference.
The template class CheckedPtr uses C++ exceptions to signal errors. To field exceptions, you place any code that may cause an exception within a try block. When the code in a try block throws an exception, control flow continues at the nearest matching catch block associated with the try block. But first, the exception mechanism calls the destructors for any automatic objects that go out of scope.
The template class CheckedPtr employs three exception classes derived from the class xCheckedPtr, which is in turn derived from xmsg, the generic exception class defined by the C++ standard header file except. h. The CheckedPtr class may throw three possible exceptions:
The source file cptest. cpp (Listing 3) uses the function test to create an environment that handles xCheckedPtr exceptions. When the catch block catches an exception object derived from xCheckedPtr class, the code in the catch block handles the exception by displaying the reason for the exception on the stream cout, using the why member function of xmsg.
- xnullptr for a dereferenced null pointer
- xoutofrange for accesses outside the range of an array to which a CheckedPtr object is limited
- xnotsamebase for pointer arithmetic with pointers that do not refer to the same array.
Template Class CheckedPtr
A template class allows you to code a generic definition of a class that you may associate with another class or C++ built-in type. Instead of writing a slightly different CheckedPtr class definition for each type of pointer that needs to be checked, you include the header file checkptr.h (Listing 1) , which defines a single template class. It lets the compiler do the work of generating instances of the class definition for each type to be associated with the class.For instance, to declare a class definition that represents a pointer to char, you write CheckedPtr<char>. When the compiler sees CheckedPtr<char> in a declaration, it uses the template class definition, which begins with template<class T> class CheckedPtr, and generates an actual class definition by substituting char for each usage of T in the template.
The template class CheckedPtr has three member objects: the pointer that the CheckedPtr object represents, a pointer to the base of an array, and a limit. For a pointer to a single object, the base is the same value as the pointer, and the limit is one. (You may think of a single object as an array with one element.) For a pointer associated with an array, the base is a pointer to the beginning of the array, and the limit is the number of elements in the array. The CheckedPtr class uses the base and limit for checking that the represented pointer and any operations on it result in a valid pointer that points to an element in the array or to the non-existent element at the end of the array.
There are two constructors for the template class CheckedPtr. The first (default) constructor initializes a CheckedPtr object with a specified pointer, limit, and base. All three parameters have default values. If constructed with no parameters, the object represents a null pointer. If constructed with a single pointer passed as a parameter, the object represents a pointer to a single object. For instance, to construct a CheckedPtr<int> object that represents a pointer to a single integer you write:
int i; CheckedPtr<int> pi= &i;For arrays, use the first two parameters to specify the base and limit of the array. For instance, for an array of 10 float, you construct a CheckedPtr<float> object like this:
float a[10]; CheckedPtr<float> p(a, 10);You use all three parameters to create an object that represents a pointer that points somewhere inside an array other than the beginning. For instance, to create a CheckedPtr<float> object that points to the third element of an array of 10 float, and is allowed to point at any element in the array, write:
float a[10]; CheckedPtr<float> p(a+2, 10, a);If the pointer passed as the first parameter does not point to an element specified by the limit and base parameters, the constructor will throw an xoutofrange exception.The template class CheckedPtr declares the copy constructor to show that it may be used. This constructor simply does a physical copy of the pointer, base, and limit member objects into the newly constructed object, like a default compiler-generated copy constructor would do.
Operators
The template class CheckedPtr provides a conversion operator so that you can pass the represented pointer as a parameter to a function that requires the underlying pointer type. For instance, by using the conversion operator you can pass a CheckedPtr<char> object as the char * argument to the strlen function.When a logical expression involves a CheckedPtr object, the expression will use this conversion operator, which mimics the use of pointer types in the same situation. For instance:
CheckedPtr<char> a(0), b(0); bool r = a || b; // returns falseThe class CheckedPtr<T> defines the member operator functions:
T& operator*() const; T& operator[](ptrdiff_t) const;which perform indirection and subscripting. These functions return a reference so that you may use the result of the expression as the left operand in an assignment statement, or in other situations that require an 1value.The member function operator[] uses an integral value of type ptrdiff_t for the right operand. C++ uses this type to represent the subtraction of two pointers of the same type. ptrdiff_t is defined in the standard C++ header file stddef.h. It is a signed type, typically either int or long. The operators for the template class CheckedPtr which require an integral operand use ptrdiff_t for this purpose.
The member function operator*() calls operator[](0), which validates the returned reference. Before operator[] returns a reference to an object represented by the operation, it first checks that the represented pointer is not null. If it is, the function throws an xnullptr exception. Next, the function checks that the resulting reference is to a valid array element. If it is not, the function throws an xoutofrange exception. It is valid for a pointer to reference the location that is just beyond the end of an array, but it is not valid to dereference that pointer value. For instance, it is valid C++ to write:
char a[10]; char *end = a+10; for( char *p = a; p < end; p++ ) ;The pointer end points to the element a[10], which does not exist.This implementation of template class CheckedPtr has an incompatibility with valid pointer usage. For instance, the following variation of the previous example is valid C++ code:
char a[10]; for( char *p = a; p < &a[10]; ++p ) ;However, if you convert this example to use CheckedPtr<char> objects instead of char pointers, like this:
char a[10]; const CheckedPtr<char> cpa(a,10); for( CheckedPtr<char> p = cpa; p < &cpa[10]; ++p ) ;then the expression &cpa[10] throws an xoutofrange exception when evaluated, because CheckedPtr<char>::operator[] does not know that the result is really not being dereferenced.A less than well-known fact is that the subscript operator is commutative in C and C++. That is, the expression p[n] is equivalent to the expression n[p]. Unfortunately, C++ does not allow the subscript operator to be a non-member function. If it did, then defining the template function:
template<class T> inline T& operator[]( ptrdiff_t i, const CheckedPtr<T>& cp ) { return cp.operator[i]; }would allow the class CheckedPtr to behave like a C++ typed pointer.
Template Class CheckedClassPtr
The other operator that dereferences a pointer, the member access operator->, is not defined for the CheckedPtr class template. Instead, the derived class CheckedClassPtr defines this operator. This is because most current C++ compilers produce an error when they generate an instance of a template class that defines a member access operator that does not return a pointer to a class object. For example, if you use the type CheckedClassPtr<char> then the generated instance of the class definition would have the member function definition:
char *CheckedClassPtr<char>::operator->() const;This is not accepted because the return type, char * is not a pointer to a class.If the C++ language definition were relaxed by allowing the member access operator to be a non-member function, it would eliminate the need for the CheckedClassPtr. If this were the case, you could define a non-member function template for the member access operator. The compiler would generate an error only if it generated an instance of the template function instead of when it generates an instance of the class definition.
[The draft C++ Standard has recently been altered in a slightly different way. The net effect, however, is to permit CheckedClassPtr to be instantiated even for non-class types. It will probably take a year or more for this new latitude to be reflected in commercially available compilers. pjp]
The member function CheckedClassPtr<T>::operator-> calls CheckedPtr<T>::operator*(), which checks that the returned pointer can be dereferenced, then returns the address of the result.
The derived class redefines the operators that return references to the class object, such as operator+=, and operators that return copies of the class object, such as operator+, to return references or objects of class CheckedClassPtr rather than CheckedPtr. This let you use the member access operator with expression that return type CheckedClassPtr, such as p++->f()
Pointer Arithmetic and Comparisons
The template class CheckedPtr defines operators that you use to perform pointer arithmetic on the pointers represented by a CheckedPtr object. Each of these operators checks that the represented pointer is not null, and that the resulting pointer representation still points to a valid array element, including the element that lies just beyond the end of an array. Even with a pointer to a single object, it is still valid to write:
char c; char *p = &c; c++; // OKThe CheckedPtr class defines the assignment operators operator+= and operator-=. These work like the same operators on pointer to object types. operator-=(n) calls operator+=(-n), which does the validation of the result. When the member function operator+= is called with a right operand of type CheckedPtr, the operator simply does a physical copy of original object. When the right operand is a compatible pointer type, the CheckedPtr object sets its internal pointer to use the right operand, treating it as a pointer to a single object.The template class CheckedPtr defines the postfix and prefix increment and decrement operators, which behave like the same built in operators for pointer types. The postfix operators return a reference to the operand object. The postfix operators, which have an (unused) int parameter, create a copy of the operand before modifying it. These operator functions return this copy as the result, which is how postfix operators behave for pointers and other built-in types.
When the member functions operator+ or operator- are called with a left operand of class CheckedPtr and a right operand of some integral type, they call in turn operator+= or operator-=, respectively. These member functions each create a copy of the left operand object and call the appropriate assignment operator for the copied object. They then return this copy as the result, leaving the original object operand unmodified. The addition operator is commutative, so the template header file defines an additional non-member function for operator+ where the left operand is an integral type and the right operand is of type CheckedPtr.
C++ defines subtraction for two pointers of the same type. The result of this subtraction is the number of objects separating the pointers. The template header file also defines subtraction operator functions that perform this kind of subtraction when at least one of the object is of class CheckedPtr. These functions check that the two pointers refer to the same array. If the operands do not, they throw an xnotsamebase exception.
The xnotsamebase exception is useful for catching pointer errors with the relational operators >, >=, <, and <=. The header file checkptr.h (Listing 1) defines these operators with at least one operand an object of class CheckedPtr. These operators call operator-, which makes sure that the pointers refer to the same array. This is useful for catching error like this:
char a[10]; char b[10]; const CheckedPtr<char> cpa(a,10); const CheckedPtr<char> cpb(b,10); CheckedPtr<char> p; for( p = cpa; p < cpa+10; ++p ) *p = 0; for( p = cpb; p < cpa+10; ++p ) // error, probably meant p < cpb+10 *p = 0;The equality operators, == and !=, simply return the result of comparing the two operands without any checking. Likewise, the logical negation operator returns the result of the same operator used on the represented pointer.
Template Class HeapPtr
One of the problems created by the introduction of C++ exceptions is that only the destructors of objects constructed on the stack are called when an exception is thrown and these objects go out of scope. If you allocate an object from the heap and store a pointer to it as a local variable, a thrown exception will cause the object to be lost along with the memory it occupies. Another common pointer error, which also leads to a memory leak, happens when you fail to delete a locally declared pointer to an object when the pointer goes out of scope.The header file heapptr.h (Listing 2) , defines the template class HeapPtr, for managing heap allocated objects. It is the responsibility of a HeapPtr object to delete the represented pointer when the object goes out of scope.
You use HeapPtr objects to allocate either a single object or an array of objects from the heap. In the first case, you create a HeapPtr object to manage a pointer to an object of type X by writing the declaration:
HeapPtr<X> Xptr( new X(/* parameters */));To create a HeapPtr object that refers to an array, use the constructor that takes a size_t argument. For instance, to allocate an array of ten objects of type X from the heap, you write:
HeapPtr<X> Xptr(10);The constructor calls new X[10].A HeapPtr object is responsible for deleting the pointer to the heap allocated object. If no copies are made of a HeapPtr object, its destructor deletes the represented pointer. But consider what happens when you make a copy of a HeapPtr object. If the original object deleted the pointer to the heap allocated object for which it is was responsible, then the copied object would reference an invalid pointer.
To solve this problem, template class HeapPtr uses an internal Ref class to manage references to a single heap allocated object. This allows more than one HeapPtr object to refer to the same heap allocated object. Internally, the object stores a pointer to a Ref object allocated from the heap. This object in turn stores a pointer to the heap allocated object. Every HeapPtr constructor other than the copy constructor initializes the internal Ref object pointer to point at a newly allocated Ref object. That object, when first constructed, has a reference count of one. When the HeapPtr object is constructed with the copy constructor, the constructed object inherits its pointer to the Ref object from the original. The common Ref object has its reference count incremented.
When you destroy either the copied or original HeapPtr object, the common Ref object has its reference count decremented. When the reference count returns to one, the pointer to the Ref object and the pointer to the heap allocated object it references are deleted.
Operators
The template class HeapPtr defines a conversion operator so that you may treat a HeapPtr object as the represented pointer in some cases. However most of the other pointer operators will not work through the conversion operator. For instance, you cannot use the subscript operator or the indirection operator. Instead, use a typed pointer or a CheckedPtr object initialized to the same value as the pointer associated with the HeapPtr object. For instance:
HeapPtr<char> hp(100); for(int i = 0; i < 100; i++) hp[i] = 0; // won't work char *p = hp; for(int i = 0; i < 100; i++) p[i] = 0; // OKYou can fix this limitation of template class HeapPtr by adding the appropriate operator functions to the class definition. If you wish, you can combine the functionality of HeapPtr and CheckedPtr by creating a multiply derived class, with appropriately redefined operators.You should not use expressions like this, however:
char *p = HeapPtr<char>(100);The unnamed HeapPtr<char> object exists only in the scope of the expression, and the returned pointer (through the conversion operator) will be invalid.HeapPtr defines three assignment operators and three function call operators that work like the three constructors. These operators have the same effect as destroying and reconstructing the object. For instance:
HeapPtr<int> hp; // null pointer hp(100); // allocate array of 100 ints hp = new int(0); // delete previous, allocate int hp = 100; // delete previous, allocate arrayThe usefulness of these operators is demonstrated in situations where they are used in tandem with a CheckedPtr object. For instance, to declare a CheckedPtr object and a HeapPtr object that represent the same pointer to an object allocated from the heap, write code like this:
HeapPtr<X> hp; // empty HeapPtr CheckedPtr<X> cp( hp(new X) ); // initialize HeapPtr and CheckPtr // to point to same objectLikewise, an array allocation looks like this:
// empty HeapPtr HeapPtr<X> hp; // allocate an array of CheckedPtr<X> cp(hp(100), 100 );Using the Template Classes
The template classes CheckedPtr and CheckedClassPtr use inline functions for speed's sake. After you debug an application, however, it may be desirable to turn off the pointer checking to avoid a performance penalty. To do this, use a conditional typedef for the pointer types that you use. The following macros create typedef statements for different pointer types:
#ifndef NDEBUG #define PTRTYPEDEF(type) \ typedef CheckedPtr<type> type##ptr #define CLASSPTRTYPEDEF(type) \ typedef CheckedClassPtr<type> type##ptr #else #define PTRTYPEDEF(type) typedef *type##ptr #define CLASSPTRTYPEDEF(type) type *type##ptr #endif PTRTYPEDEF(char); // defines type charptr CLASSPTRTYEPDEF(complex); // define type complexptrThis conditional typedef introduces an incompatibility with existing pointer usage. Consider what happens when you declare a const charptr. Then const char * refers to a pointer that may be changed, but may not have the data to which it points changed. But a const CheckedPtr<char> means that the object may not be modified, but the data to which the pointer it represents points may be modified. For example, the declaration:
const CheckedPtr<char> p;is analogous to the declaration:
char *const p;The solution is to use another macro that defines const pointer types:
#ifndef NDEBUG #define CONSTPTRTYPEDEF(type) \ typedef CheckedPtr<const type> \ const##type##ptr #define CONSTCLASSPTRTYPEDEF(type) \ typedef CheckedClassPtr<const type> \ const##type##ptr #else #define CONSTPTRTYPEDEF(type) \ const type *const##type##ptr #define CONSTCLASSPTRTYPEDEF(type) \ const type *const##type##ptr #endif CONSTPTRTYPEDEF(char); // defines the type constcharptrTo create checked pointers to static and auto arrays, use the following conditional macro:
#ifndef NDEBUG #define FIXEDARRAY(array) \ (array),sizeof(array)/sizeof((array)[0]) #else #define FIXEDARRAY(array) (array) #endifUsing this conditional macro, your code would look something like this:
char a[10]; charptr pa(FIXEDARRAY(a)); for( charptr p = pa; p < pa+10; ++p ) *p = 0;For dynamically allocated arrays that are managed by a HeapPtr object, use a conditional macro like this:
#ifndef NDEBUG #define HEAPPTRARRAY(hp,n) hp=(n),(n) #else #define HEAPPTRARRAY(hp,n) hp=(n) #endifWith this macro, the previous example converted to use a heap-allocated array becomes:
// nothing allocated HeapPtr<char> ha; // array of 10 charptr pa(HEAPPTRARRAY(ha,10)); for( charptr p = pa; p < 10; ++p ) *p = 0; // ha deletes array when it goes out of scopeIn summary, These class templates are useful debugging tools for developing C++ code. By using these class templates, you can detect many pointer errors when they happen, rather than helplessly observing the side effects, resulting in long debugging sessions, missed deadlines, and hair loss.