[ Summary ] [ Download Files ] [ News ] [ Forums ] [ Bugs ]
Table of Contents:
In C++, pointers are very commonly used, but the current "built-in" pointers
leave something to be desired. When a built-in pointer is created, it is not automatically set to NULL.
If an uninitialized pointer is then compared to NULL (pointer ==
NULL) the test will pass, and any dereferencing will result in undefined behavior. It's fairly easy to remember to set a pointer to NULL when you create it, so this issue isn't that important, but what if you call a function that returns a pointer? If the memory was allocated on the heap (i.e. came from a call to new or malloc) then someone has to delete it, or it will be a memory leak. It's up to the programmer to read the documentation and figure it out.
What about in a multi-threaded environment? It's very easy for two threads to share the same
data, but what if both of them are using the same pointer, and one thread calls delete on the pointer while the other thread is still using it?
Finally, if you're using exceptions, you've probably had a pretty hard time making sure that each time an
exception is thrown, all of the allocated memory gets freed. Take the following code:
try { int* pInt = new int; SomeFunction(); . . . delete pInt; } |
What if SomeFunction throws an exception? You have to make sure that each and every exception that SomeFunction (or any function called by SomeFunction) throws
will be caught, in order to delete pInt; otherwise you'll have a memory leak. Certainly there must be a better solution than using these built-in "dumb" pointers. Well, there is, they're called "Smart Pointers", and they handle
all of this stuff for you.
The idea behind this Smart Pointer implementation is to have a set of templated objects that wrap the functionality of a built-in pointer. These Smart Pointers either "point" to an object or they equal NULL (they never point to memory that has been deleted and they are always initialized). Here are some of the goals that I had for this project:
The pointers must always point to valid memory, or be NULL
The pointers will be reference counted and handle freeing the memory being pointed to (so they can be exception safe while at the same time eliminating memory leaks)
The pointers should be as similar to the built-in pointers as possible
In order to accomplish goal 1, the pointers had to be thread safe, which usually means platform specific, but this implementation will work on both Win32 and POSIX compliant (Linux, BSD, Solaris, Mac OS X, etc.) Operating Systems.
No matter how many threads you have, your pointers will never point to invalid
memory.
Goal 2 required that the pointers be exception safe. When an exception is thrown, the stack unwinds until it finds some code that will catch the
exception. As the stack is unwinding, the destructors of each object are called. When the Smart Pointer destructor is called, it will remove one reference from the count associated with the object it is pointing to. If the new reference count is zero,
that object will be deleted. So, if an exception is thrown,
as long as a Smart Pointer is pointing to the object the was allocated on the heap, everything will be fine and the memory will be
freed.
The third goal was particularly difficult to accomplish. To do this, I had to implement implicit casting (casting without a casting operator), explicit casting through casting operators like dynamic_cast and reinterpret_cast, and the pointers had to be "const" correct.
Additionally, in order for the smart pointers to act like the built-in types, it was
necessary for the implementation to be non-intrusive. An intrusive smart pointer is one that requires a common base class be used in any object that
will be pointed to. When you create your custom objects, you would have to inherit from a base class the
gives reference counting functionality to
your object. This approach only works for user defined types, and the Smart Pointers
would never be able to point to built-in types (like int and float). A
non-intrusive approach requires no changes to a defined type (whether built-in or
user defined) because the pointer has a more intelligent means of keeping track of the
reference count.
Along with reference counted pointers comes the ugly problem of circular
references. A circular reference is when two reference counted objects are
keeping each other "alive" even though neither is still in use. For
example, two objects are defined, one which is a Parent object and the other a Child
object. As a member variable, the Parent object has a Smart Pointer pointing to
the Child objects, while the Child has a Smart Pointer pointing to the Parent.
The reference counts will never get to zero, and therefore the memory will never
get deleted. This problem has been solved through the use of Inactive Pointers,
which I will explain later.
All of the above goals have been met by this implementation. The programmer doesn't have to worry about memory management (never see the word "delete" again!), the pointers are copy safe, thread safe, exception safe, and they never point to memory that has already been deleted. If used properly, they avoid circular references, and they work almost identically to the built-in "dumb" pointers.
To use the smart pointers in a C++ source file, just add the following line of code:
#include "Smart Pointers.h"
That's it. You don't have to add any files to a project, or any
dependencies to a makefile (unless you plan on changing the Smart Pointer code).
Everything that you need is included in that file, but you still need more info
than that. Here are the two main Smart Pointers which are defined:
Ptr< type > // also known as "Active Pointer"
type is a data type, like an int or some user defined type; if the type is const, then the object being pointed to cannot be changed, and only const member functions can be called
Ptr is the main reference counted pointer. A raw pointer is passed in to the constructor (Note: this raw pointer must be memory that was allocated on the heap by a call to the new operator; if the address of data on the stack is passed in, the program will crash)
You can use Ptr just like a regular pointer; operator->, operator*, operator=, operator==, and operator!= are all overloaded
ResetPtr() is a public member function that can be called on the Smart Pointer. It removes one reference count, and sets the pointer to NULL
GetRawPtr() is a public member function that will return the address of the object being pointed to. This method is only implemented for API and Library calls which require a built-in (raw) pointer. Raw pointers do not get the protection that a Smart Pointer has, but as long as one or more Smart Pointers point to the object, the object won't be deleted and the raw pointer will still be valid
Here is some sample code:
class Base{}; ... Ptr<Derived> pDerived;
// Smart Pointer do Derived object; Ptr<const Base>
pcBase; // Smart Pointer to const Base object; const Ptr<int>
pInt( new int
); // const pointer, can't be pDerived = new
Derived; // create a Derived object pDerived.ResetPtr(); // same as pDerived = NULL;
// pcBase still points to the object |
InactivePtr< type > // also known as "Inactive Pointer" or "Passive Pointer"
type is a data type, like an int or some user defined type; if the type is const, then the object being pointed to cannot be changed
InactivePtr is only meant to be used in situations where a circular reference is present. InactivePtr is a non-reference counted pointer. The object it points to must have at least one Regular (or Active) Pointer pointing to it. Since InactivePtr is not reference counted, in a multi-threaded environment it can spontaneously become NULL. Due to the instability of InactivePtr, most of its functionality must be taken away; to use the address stored in an InactivePtr, you must create an Active Pointer (Ptr) and initialize it with the InactivePtr (i.e. call the overloaded constructor passing the InactivePtr in as an argument). Once in an Active Pointer, all operations can be carried out safely, but the Active Pointer must first be checked for NULL.
InactivePtr is practically useless (but necessary to combat circular references). It does, however, give you the guarantee that what is being pointed to is either valid or NULL (but never deleted or uninitialized)
ResetPtr() is a public member function that can be called on the Smart Pointer. It sets the pointer to NULL
GetRawPtr() is a public member function that will return the address of the object being pointed to. This method should never, ever, ever be called. (It is only implemented because Microsoft's compiler doesn't allow template classes to be friends of each other.) In a multi-threaded environment, the raw address returned by GetRawPtr() could spontaneously become invalid (it wouldn't be set to NULL because it's just a raw address, not a Smart Pointer). If you need the address of what is being pointed to by an InactivePtr, first create an Active Pointer (Ptr), and initialize it with the InactivePtr (i.e. call the overloaded constructor passing the InactivePtr in as an argument), then call GetRawPtr() on the Active Pointer.
Here is some sample code:
InactivePtr<int>
ppInt; // Inactive Pointer to
int;
} // the active pointer
will go out of scope, destructing Ptr<int>
pActiveInt( ppInt ); // create an Active
Pointer if(
pActiveInt != NULL )
// you must always check an Active
|
The Smart Pointer casting operators work almost exactly like the C++ style casting operators. Just in case you don't remember the C++ casting operators, here's a quick review:
reinterpret_cast< type >( expression )
Only used for converting one pointer to another. Converts the pointer in the expression to the specified data type. This is done at compile time and should incur no run-time penalty. It's just a simple way to tell the compiler that you are converting from one pointer type to another pointer type, and you don't want it to complain.
const_cast< type >( expression )
Removes the cv-qualification from an expression. cv-qualification is the const or volatile attributes that the expression has. This is usually used to take the constness away from a variable.
static_cast< type >( expression )
Casts the expression to the type if the conversion is possible. This operator will make a direct call an overloaded casting operator for a class, if one is present. It is not typically used with pointers, so there is no Smart Pointer version.
dynamic_cast< type >( expression )
This cast only works with polymorphic types (types with at least one virtual function). It is usually used for upcasting (casting from a base object to a derived one). If a cast from one pointer to another cannot be performed (because the pointer wasn't really of the derived type), the operator will return NULL. When the cast can't be performed while casting from one reference to another, the std::bad_cast exception is thrown.
So that's the C++ style casting operators. The Smart Pointer specific versions are quite similar, with one major exception. In the C++ operators, if you want to cast from a pointer to a pointer, you must specify that in the type field. For example:
int* pInt = dynamic_cast<int*>( SomePointerToInt ); |
The * symbol is in between the less than-greater than symbols is a necessity. In the Smart Pointer version, it is assumed that you are casting from one pointer to another, so the * symbol is eliminated (basically, I couldn't get it to work any other way):
Ptr<int> pInt = Dynamic_Cast<int>( SomeSmartPointerToInt ); |
Also, you'll notice the the 'D' and 'C' are capitalized. The casting
operators can't be overloaded, so I had to come up with similar names:
Reinterpret_Cast
Const_Cast
Dynamic_Cast
(You'll notice that there is no Static_Cast. Since there is no reason to use
static_cast on built-in pointers, there isn't a reason to have a Smart Pointer
version.)
There are two ways that a Smart Pointer can be const:
1) the pointer itself is const and can't be reassigned
ex: const
Ptr<int>
2) the object being pointed to is const and can't be modified
ex:
Ptr<const int>
Const_Cast is only useful in casting away the constness of the second example.
All of the Smart Pointer casts work regardless of which type of Smart Pointer is being used (i.e. Active to Active, Active to Inactive, etc.), except for Dynamic_Cast. You can only use it to cast from Active Pointer to Active Pointer, or from Active Pointer to Inactive Pointer. The reason for this has to do with the unstable nature of Inactive Pointers. (If Dynamic_Cast used thread synchronization constructs, then this function could work, but that would be a pain to implement correctly, since Dynamic_Cast is not part of an object. If there are enough requests for this though, I'll implement it.)
When I say that the Smart Pointers are "thread safe" it means that they will work correctly in a multi-threaded environment. The important thing to realize is that the Smart Pointers, and only the Smart Pointers, are thread safe. The objects that are being pointed to, are not thread safe. If multiple threads are accessing the same object through two different Smart Pointers, the pointers will still hold the guarantee of pointing to valid memory, but the object is not thread safe, it is up to the programmer to do this.
Nevertheless, there are a few tips to ensuring that the Smart Pointers themselves remain thread safe. First, if you always pass the Smart Pointers by value, then you'll never have to worry about anything. Since the Smart Pointers are reference counted, they will take care of everything on their own, but what if you have to pass a pointer into a function, and the function will change what the pointer points to? In C you would use a pointer-to-a-pointer, and in C++ you could use a reference-to-a-pointer, but references and built-in pointers, are not thread safe. For example, if you have a reference to an object created on the heap, and then another thread decides to go ahead and delete that object, the next time you try to use your reference, bad things will happen. So back to the question, how do you pass a Smart Pointer into a function that will change what the Smart Pointer points to? If you pass the Smart Pointer in by reference, the code will still be thread safe if you have at least one active pointer pointing to the same object and on the same thread. If you pass an actual Active Pointer into a function, by reference, then you'll be ok. If you have a reference to an Active Pointer, which you received from another thread, and you pass that reference into a function, then your code would not be considered thread safe. Just remember that references can be thread safe if used properly, but it's best to pass Smart Pointers around by value if you can.