containers/smart_ptr.hpp
Smart Pointer Containers

Introduction

The problem with using pointers to objects is that the object "leaks" if it is not deleted when the last pointer to it goes out of scope, or it the data structure containing it is itself deleted. It may seem obvious that the object should be deleted, but actually this decision is very difficult in a complex program - often the place where memory is allocated is remote from where it is deallocated and so objects can be left to leak. An even bigger problem is that objects are held in more than one structure and end up being double-deleted, which corrupts the memory management. The result is that memory management tends to dominate the design of large programs. Many attempt to solve this the hard way have been developed, such as using garbage collection, but in fact there is a solution based purely on using container classes to manage all memory. The smart pointer is a part of this solution.

A smart pointer is a class that acts like a pointer but which also deletes the object pointed to when finished. It is, if you like, a memory-managing pointer.

A smart pointer maintains a pointer to a single object. It also keeps a count of how many smart pointers point to the object. When smart pointers are assigned or 'copied', no copying of the object pointed to takes place, instead the pointer count is just incremented. This is known as aliasing. When a destructor is called, the count is just decremented. This is known as dealising. The object itself is destroyed when the count is decremented to zero, indicating that the last alias has been destroyed.

The existing STL containers already memory manage their contents - for example, when a vector goes out of scope, its destructor will deallocate its contents. However, there are some situations where you need to store pointers, not objects, in containers. Container classes will not deallocate those. These are the situations where a smart pointer is needed. A vector of smart-pointers will manage it's memory.

To illustrate this need, here's my top three situations where smart pointers are required:

Reason 1 - C++ Polymorphism Must be Implemented as Pointers to Objects

In C++, you manipulate families of derived classes through a pointer to the base class. You can then access virtual functions by dereferencing the baseclass pointer. For example

class base
{
  ...
  virtual void print(void) const;
};

class derived : public base
{
  ...
  virtual void print(void) const;
};

...

base* b = new derived;
b->print(); // actually calls derived::print

The reason that you must use pointers here is that classes base and derived could be of different sizes - so you cannot assign a derived to a variable of type base because it might not fit. This rule is enforced by C++. However, you can assign a (derived*) to a (base*) as in the example because class pointers are always the same size. Thus, pointers have to be used to implement polymorphism in C++.

However, as explained above, this makes memory problems an issue. The solution is to use smart pointers (the cloning variant in the case of polymorphic types) rather than simple pointers. Then the memory is managed by the smart pointer - if a pointer becomes inaccessible either because it's container data structure is destroyed, a function returns or because an exception is thrown, then all its contents will be destroyed correctly by the smart pointer's destructor.

smart_ptr_clone<base> b = new derived;
b->print(); // actually calls derived::print

Reason 2 - Minimising Reallocation Overhead

The vector is one of the most useful template classes in the STL. It has one potential problem: when a vector has to resize, it does so by copying all of its contents into a new vector. It does so by using the copy constructor of the contained type. Therefore a vector of 1000 objects results in 1000 calls to the copy constructor.

This is not itself a problem if the constructor is trivial since the call overhead is negligible. However, if the type stored in the vector is big and/or complicated, then the copy overhead can become serious. Storing a pointer to the object in the vector is not a good idea - the memory will then not be managed by the vector as explained above in Reason 1. The solution is to use a vector of smart pointers to objects. Then, when the vector resizes, it copies the smart pointer, not the object. The overhead of copying a smart pointer is negligible - only slightly more than a simple pointer. Then, when the vector is destroyed, its contents - smart pointers - are destroyed and they in turn delete their contained objects

Reason 3 - Returns Must be Implemented as Pass-by-Value

It is often useful to write functions that create data structures and return them. However, there are problems with this.

Passing back a reference to a local (automatic) variable is illegal, since the variable is destroyed in the return so that the reference returned now refers to destroyed memory. This is a well-known pitfall in C++.

Creating the object with new and passing back a pointer to the object seems to be a good solution. The problem with this is that the returned value is not memory managed and will leak in the case of an exception being thrown. It can also make code difficult to read and debug because the place where the data structure is allocated is a very different place to where it is destroyed. This separation of new and delete is a well-known source of memory bugs - inevitably the question of which part of the program is responsible for the deallocation becomes confused and you can end up with either leaky code or double-deletion and the memory corruption that results.

If you return by value, this causes a copy constructor to be called. This is fine for small data structures where the copy overhead is correspondingly small, but it is undesirable if the data structure is big and impossible if the data structure cannot be copied (occasionally it is necessary to design data structures that are not copiable).

The solution is to create the object in a smart pointer and return it by value. Pass by value with a smart pointer just creates an alias and so has negligible overhead. However, the smart pointer is managing the object's memory, so the program does not need to remember to delete it later. It will delete itself when it is no longer in use.

Summary

I think the days of tracking dynamic memory lifetimes through layers of software and running leakage diagnostics in programs is now well past. The smart pointer classes combined with the other STL and STLplus container classes make it possible and practical to make every data structure self-managing.

In this style of programming, the delete operator is simply never used, since all objects are being managed by a container. The advantage of this approach over Java is that no garbage collection is needed because objects are deleted properly when they go out of scope rather than drifting off in the vain hope that the garbage collector might eventually spot them. So although the delete operator is never used, there will be no memory leaks.

The STL doesn't deal with this issue, it doesn't provide a single-object container (or at least not a useful one - there is a class called auto_ptr but it is inadequate), which is a major oversight. The smart pointer classes are my solution to the issue.

Update: the C++ 2011 standard includes a smart pointer in the STL at last. You are recommended to use that class, but the STLplus will continue to provide smart pointers for backwards compatibility.

To illustrate this approach, when I wrote the smart pointer classes I was about to embark on a two year programming project and decided to use the self-managing principles outlined above. The approach works well and creates efficient and readable code. There is not one single call to delete anywhere in over a megabyte of application code that I'd written in that time. When analysed with a leak testing program, there was not a single memory leak nor double-deletion nor buffer overrun anywhere.

What Are Smart Pointers?

A smart pointer is basically a pointer with a destructor. The destructor ensures that the object pointed to is deleted when it is no longer being used. You can have multiple pointers to the same object, so the object is only deleted when the last pointer is destroyed.

A smart pointer has to have slightly different characteristics to a conventional C pointer. These differences show up in the behaviour of the pointer on assignment and when the pointer's address (i.e. the object being pointed to) is changed. Deciding the exact behaviour is difficult and very subjective. This could be solved by having a range of different classes with slightly different behaviour. You could then choose which one suits your problem. However, I don't agree with this approach - I think you get quickly bogged down with the subtle differences and lose sight of the program you are actually trying to write. So I provide just one kind of smart pointer which I believe is the most general-purpose. I think this is consistent with the STL, which provides just one kind of vector, map, list etc.

The smart pointer contains the address of the object pointed to and a counter which stores the alias count. When one smart pointer is assigned to another, the second smart pointer becomes an alias of the first, which means that it is pointing to the same object. The alias count of the object is incremented to show that there are now two pointers pointing to the same object.

In fact, an extra level of indirection needs to be used. A smart pointer class in fact contains a pointer to a sub-object which in turn contains the address of the contained object and an integer containing the alias count. This is illustrated in Figure 1:

Smart pointer structure
Figure 1: The structure of class smart_ptr

In this figure, there are two smart_ptr objects pointing to the same sub-object (which is actually part of the smart_ptr structure hidden from the user), which then points to the data object which is the user's data. The alias count is stored in the sub-object. There is an alias count of 2 because there are two smart_ptr objects pointing to the sub-object.

When a smart pointer goes out of scope or is destroyed for any reason, it is dealiased. This means that the smart pointer no longer points to the object. It's alias count is decremented to show that one less smart pointer is pointing at the object. When the alias count decrements to zero, this indicates that the object is no longer being pointed to by any smart pointers and so it is deleted automatically.

A consequence of the smart pointer design is that if you change the object pointed to by one smart_ptr, you change the object pointed to by its aliases too. In other words, if you change the address of one smart_ptr, you are actually changing the address stored in the sub-object, which is visible to all other aliases, so these appear to change their stored address too. The result is that all aliases end up still being aliases and all pointing to the new object, whilst the old object gets deleted automatically because it is no longer pointed to.

This behaviour is unlike conventional C pointers - if you allocate a new object to a pointer variable in C, you don't expect other pointer variables to change too. However, it can have very significant advantages in data structure design. It is common when designing large and complex data structures to have several different ways of accessing objects - for example you might store an object in a map which allows access to objects sorted in alphabetical order, but also store it in a vector which allows access to objects in the order in which they were created. Traditionally this could lead to problems in memory managing the object when removing it from one or other since it could become ambiguous which data structure is the container responsible for deallocating its memory. However, with smart pointers this problem disappears - put a smart pointer in both map and vector pointing to the same object, i.e. aliases. The object will persist until both smart pointers are deallocated. Furthermore, if the object pointed to needs to be changed, it can be changed by changing either alias. So, you can look it up in the map, change the address and the address pointed to by the other alias in the vector will automatically change to the new object - they remain consistent.

This is the thinking behind the smart pointer design, even though it seems strange and a bit counter-intuitive to people unused to using smart pointers. In my opinion the power of the design outweighs the slight strangeness.

Smart Pointer Variants

The STLplus smart pointer has three variants. The variants differ only in their ability to create copies of the object pointed to:

template<typename T> class stlplus::smart_ptr
This is a smart pointer intended for use on simple types and single classes. It uses the copy constructor to copy the object of type T that it contains.
template<typename T> class stlplus::smart_ptr_clone
This is a smart pointer intended for use on polymorphic types - that is families of classes and subclasses which are manipulated through a pointer to their base class. It uses a clone() method to copy objects.
template<typename T> class stlplus::smart_ptr_nocopy
This is a smart pointer intended for use on objects that cannot be copied - for example a class that contains a system resource other than memory where copying makes no sense, or a data structure too complex to implement a copy operation or where there is simply no need to be able to copy it. It is like smart_ptr but has all operations that cause copying of the pointed-to object stripped out.

Although there are three smart pointer classes, they have almost identical interfaces, so they are described together here (in fact, they are all instances of a base class called smart_ptr_base, with each variant a derived class of that with a different copy-functor). I use the most common variant - smart_ptr - as the basis of this description and then describe the differences with the variants at the end.

Constructing and Destroying

There are four constructors and of course a destructor for smart_ptr:

smart_ptr::smart_ptr(void);
explicit smart_ptr::smart_ptr(T* data);
smart_ptr::smart_ptr(const T& data);
smart_ptr::smart_ptr(const smart_ptr<T>& r);
smart_ptr::~smart_ptr(void);

The first of the constructors simply creates a null pointer (it is perfectly legal and correct to have a null smart_ptr).

The second constructs a smart pointer from a pointer to an object. This pointer must be created using new (if you pass the address of an automatic variable, things will go horribly wrong). For example:

stlplus::smart_ptr<std::string> s1 = new std::string("Hello World");

The third constructs a pointer from an object. Note however that the smart_ptr copies the object passed in this way. It does not manage the original object. This is because the smart_ptr must contain an object created dynamically (on the heap) using new, and ensures this by doing the allocation itself. To do the allocation yourself, use the second constructor.

Note: in the smart_ptr_nocopy variant, the stlplus::illegal_copy exception will be thrown, since objects cannot be copied. In this case, only the second constructor can be used.

The final form of constructor is the copy constructor - but it doesn't copy the contents of the pointer. Instead it implements the aliasing behaviour of the smart pointer classes.

The destructor implements the other half of the aliasing behaviour. When a smart pointer is destroyed, the alias count is decremented. If this makes it zero, then the object contained by the smart pointer is deleted.

A simple example illustrates how a pointer is constructed and initialised in a single step:

stlplus::smart_ptr<stl::string> s1(stl::string("testing"));

This calls the object copy constructor (the third one above). It creates a string containing the word "testing" and then creates a smart pointer pointing to a copy of that string. The original string will be destroyed immediately afterwards because it is a temporary object.

In fact, because the string class has an implicit type conversion from char*, it can be written more simply than this:

stlplus::smart_ptr<std::string> s2("testing");

Here's another example which constructs two smart pointers to point to the same string:

std::string s = "testing";
stlplus::smart_ptr<std::string> s1 = s;
stlplus::smart_ptr<std::string> s2 = s1;

This example takes advantage of the C++ convention that an initialisation expressed as an assignment is in fact a call to the constructor. The first line creates a string initialised with the value "testing". The second line creates a smart pointer initialised with a copy of this (remember that the object contained by the smart pointer is copied into it). The third line creates another smart pointer which is an alias of the first one. Note that the second smart pointer is initialised from the first smart pointer, meaning they point to the same internal object.

Since both s1 and s2 point to the same string, modifying the string though s1 will result in the string pointed to by s2 apparently changing. However, the string in s1 and s2 is a copy of the original string s, so changing s will not change s1 or s2.

Typically the object is constructed as an anonymous temporary in the call to the smart_ptr constructor:

stlplus::smart_ptr<std::string> s1(std::string("testing"));

Note that an anonymous temporary object can be constructed by using the class name (in this case "std::string") as if it was a function call. This is a common convention in C++.

Assignments

As is the usual convention with C++ classes, there is an assignment operator corresponding to each constructor with equivalent behaviour (except of course for the void constructor).

First, there is an assignment that allows the object pointed to by smart_ptr to be replaced by a new-allocated object:

smart_ptr<T>& smart_ptr::operator=(T* data);

So for example, you can write:

s1 = new std::string("Hello World");

Once again you must allocate this with new. If you pass a pointer to an automatic variable, the smart pointer will later try to delete it. Similarly, don't pass the address of an object already contained in another data type - both containers will try to delete it.

The purpose of this function is to allow an object to be allocated and then handed over to the smart pointer to be managed by it without incurring the overhead of a copy operation. Assigning an object (rather than a pointer to the object) to a smart pointer causes a copy of the object to be made. This is fine for simple types and data structures since the copy overhead is small. For large data structures however, the copy overhead can become excessive. Furthermore some objects cannot be copied. In this case it is more efficient to use pointer assignment for this. However, it is potentially dangerous in that if an invalid address is passed in (for example the address of an automatic variable) then all hell will break loose.

Note: It is your fault if you get caught out by this. I have warned you to be careful! A good guideline is to only ever initialise a smart pointer this way by using the return value of the new operator.

When you assign to a smart pointer in this way, not only that smart pointer but all aliases of it end up pointing to the new object.

Furthermore, assigning a null pointer will clear the smart pointer and all of its aliases.

smart_ptr<T>& smart_ptr::operator=(const T& data);

This assignment of the contained type T to the smart pointer changes the value pointed to by this smart pointer and of all other pointers that are aliases to it. It deletes the old value and then creates a new object which is a copy of the parameter.

For example:

s1 = "Hello World";

This is slightly strange behaviour - you are assigning a string to a smart_ptr<string>! With simple pointers this would be illegal, but smart pointers allow it. The old value of the string will be discarded and the new value copied into its place.

There's a final form of assignment:

smart_ptr<T>& smart_ptr::operator=(const smart_ptr<T>&);

This assignment of a smart pointer to another dealiases this smart pointer from any other aliases and deletes the contents if this was the last alias to it. It then makes this smart pointer a alias to the parameter.

Setting a simple pointer

void smart_ptr::set(T* data = 0);

This function is functionally identical to the assignment operator:

smart_ptr<T>& smart_ptr::operator=(T* data);

It is present in the class because some people prefer its more explicit form.

Logical Tests

As the previous section showed, it is possible for a pointer to be null. Of course, it is therefore necessary to be able to test for a null pointer. The smart pointer has a set of four test functions, tests for null and non-null in two forms; explicit (function) form and implicit (operator) form. The implicit (operator) form is provided by the following operators:

smart_ptr::operator bool(void) const;
bool smart_ptr::operator!(void) const;

The following examples show how to use these:

if (s1)
  <non-null handler>
else if (!s2)
  <null handler>

Alternatively, use the explicit (function) form:

bool smart_ptr::present(void) const;
bool smart_ptr::null(void) const;

In use:

if (s1.present())
  <non-null handler>
else if (s2.null())
  <null handler>

Dereferencing Operators

The smart pointer classes look quite like pointers in use.

To dereference a smart pointer, simply use the * operator just like a simple pointer. In fact there are two variants of this:

T& smart_ptr::operator*(void);
const T& smart_ptr::operator*(void) const;

In other words, if you dereference a non-const smart_ptr you get a non-const reference to the object which you can then read from or write to. If you dereference a const smart_ptr, you get back a const reference which is therefore read-only.

Dereferencing a null smart pointer causes the stlplus::null_dereference exception to be thrown. Therefore if you are unsure whether a smart pointer is null, use one of the logical tests to check first.

Similarly, just as with simple pointers, to dereference and access a member in one go, use the -> operator. There are two forms of this too:

T* smart_ptr::operator->(void);
const T* smart_ptr::operator->(void) const;

So, if you dereference a non-const smart_ptr you get a pointer to a non-const object and therefore can call non-const methods. If you dereference a const smart_ptr you get back a pointer to a const object and therefore can only call const methods of that object.

These operators can only be used as a prefix to a method call or member access. Because of this it is illegal to use them to access a null pointer and the same stlplus::null_dereference exception is thrown as with operator*.

There are functions with the same behaviour as these operators, to provide a more explicit access to the pointer or value pointed to:

void smart_ptr::set_value(const T& data);
T& smart_ptr::value(void);
const T& smart_ptr::value(void) const;

The set_value method allows the contents of a smart pointer to be changed:

s1.set_value("Hello World");

The value methods allow access to the object in a way similar to the * operator:

std::cout << s1.value() << std::endl;

There is also an explicit form of the -> operator:

T* smart_ptr::pointer(void);
const T* smart_ptr::pointer(void) const;

Hint: The pointer() function does not throw an exception, so it is unlike the operator-> form in that way. If the smart pointer is null, then it returns a null pointer.

To show how these operators are used, consider the previous smart pointer to a string examples:

*s1 += " 1, 2, 3";
s2->insert(s2->begin(), "testing, ");

Note that the +=, insert and begin methods all come from the std::string class.

This leaves both s1 and s2 pointing to a string containing the text "testing, testing 1, 2, 3".

Rewriting to use the functions rather than the operators:

s1.value() += " 1, 2, 3";
s2.pointer()->insert(s2.pointer()->begin(), "testing, ");

Forcing a Copy

It is sometimes desirable to detach two smart pointers to the same object by creating a new copy of the object and making one pointer point to the new object. This is achieved with the make_unique method. What this does is force the pointer being made unique to point to a unique copy of the object. If the pointer is already unique, it has no effect, but otherwise it creates a local copy unique to itself. For example, to make s1 and s2 in the earlier examples point to separate strings, do this:

s2.make_unique();

It is also possible to test whether two pointers point to the same object by using the aliases method, which returns a bool and is usually used in a conditional:

if (s1.aliases(s2))
  s2.make_unique();

It is also possible to assign a unique copy by using the copy method:

s1 = s2.copy();

The copy method returns a new smart pointer which contains a unique copy of the contents of the object - in this case s2. This smart pointer is then copied to s1 using the assignment operator - which makes the assignment efficiently by using alias counting methods. The end result is that s1 and s2 now point to different strings containing the same text.

There is an alternative form of the copy function:

s1.copy(s2);

This is functionally equivalent but saves an assignment - which is negligible anyway. However, some people prefer this style.

Note: in the smart_ptr_nocopy variant, the stlplus::illegal_copy exception will be thrown in all these examples.

Clearing

There are two functions which clear the contents of a smart pointer, making it a null pointer:

void clear(void);
void clear_unique(void);

To make a smart pointer null - i.e. to force the object pointed to to be deleted right now, simply call the clear method:

s2.clear();

Warning: this deletes the object and makes the pointer null. It does not make the pointer unique first! Thus any other smart pointers pointing to the same object will become null too.

To clear only one alias while leaving all others still pointing to the object, call clear_unique. This clears just this smart pointer by making it unique and null at the same time.

s2.clear_unique();

This makes s2 null and unique, leaving s1 still pointing to its original value.

Exceptions Thrown

The smart pointer classes can throw two exceptions:

stlplus::null_dereference
Thrown if a smart pointer is dereferenced when it is null.
stlplus::illegal_copy
Thrown by the smart_ptr_nocopy variant if any method is called that would require the object to be copied.

See also the STLplus exceptions policy for a discussion of how exceptions are used in the library

Clone Variant

The discussion so far has concentrated on the variant called smart_ptr. This is designed to be used on simple types and classes where a copy can be made by simply calling the copy constructor for the contained object. However, when using hierarchies of derived classes (also known as polymorphic classes), this is not possible because copy constructors are not virtual, so you cannot make a copy this way. This is a well-known problem with C++'s implementation of polymorphism.

The solution to the non-virtual copy constructor problem is suggested by Ellis and Stroustrup in "The Annotated LRM". The solution is to require the user to always provide a virtual clone method for every polymorphic class. The clone method makes a copy using new and the correct copy constructor for its own derivative class, returning the result as a pointer to the base class. Each derived class overloads this function with its own variant which copies its own type. Thus the copy operation is now virtual (since the clone method is virtual) and furthermore is localised to the individual derived classes.

The cloning variant, a class called smart_ptr_clone, uses the Ellis and Stroustrup proposal to solve the problem of providing smart pointers for polymorphic types. It works by replacing all copy operations with a call to the virtual clone method. This does mean that it will only work with classes that have the clone method!

As an example, consider the simple example of two classes, a base class called base and a derived class called derived:

class base
{
  ...
};

class derived : public base
{
  ...
};

To make these classes suitable for use in a smart pointer, they must be made clonable. This is done by adding the clone method to both classes:

class base
{
  ...
base* clone(void) const {return new base(*this);}
};

class derived : public base
{
  ...
base* clone(void) const {return new derived(*this);}
};

Note: the clone method must return a pointer to the base class, not the derived class. This is because C++ disallows overloading with different return types. In this case, both clone methods return a base*. Notice how each clone method copies it's own class by calling new followed by the copy constructor for its class.

You then create a smart pointer for this by using the base class as the template parameter:

typedef stlplus::smart_ptr_clone<base> base_ptr;

The smart_ptr_clone class is identical to the smart_ptr class in its interface. The only difference is in its implementation. In every case where smart_ptr would copy the contained object, smart_ptr_clone clones the object instead.

Nocopy Variant

This variant of the smart pointer is designed for use on objects that cannot (or must not) be copied. An example would be when managing an object that contains, say, a file handle. It is essential that this not be copied because then you get the problem of deciding which copy is responsible for closing the file. To avoid the problem, wrap the file handle in a class and then manage a unique instance of it using a smart_ptr_nocopy. This ensures that the file handle cannot be copied and is closed when the last alias is destroyed.

The interface to the nocopy variant is the same as smart_ptr but with all operations that perform copying forbidden. In fact, because all three variants are instances of a common base class, the forbidden methods do exist but will throw the exception stlplus::illegal_copy if they are called.

The following operations from smart_ptr_nocopy cannot be used because they use copying of the pointed-to object and will therefore throw the stlplus::illegal_copy exception:

smart_ptr_nocopy::smart_ptr_nocopy(const T& data);
smart_ptr_nocopy<T>& smart_ptr_nocopy::operator=(const T& data);
void smart_ptr_nocopy::set_value(const T& data);
void smart_ptr_nocopy<T>::make_unique(void)
void smart_ptr_nocopy<T>::copy(const smart_ptr_nocopy<T>& data)
smart_ptr_nocopy<T> smart_ptr_nocopy<T>::copy(void) const