Smart Pointer Tutorial

Smart pointers are a very helpful tool to avoid manual memory management and all the issues connected with it (memory leaks, unclear ownership semantics, double deletions, exception safety, ...). Basically, smart pointers are class templates that encapsulate a raw pointer and free memory in the destructor. This makes RAII possible, a fundamental idiom for resource management in modern C++.

C++ libraries contain various smart pointer implementations which mainly differ in the way ownership is managed. The most important ownership semantics are:

Design decisions

For many use cases, the well-known smart pointers (most notably shared_ptr and unique_ptr) are not enough. Aurora provides the class template aurora::CopiedPtr which implements a smart pointer with copied ownership. It has been conceived with the following points in mind:

Basic operations

The operators * and -> for dereferencing as well as a conversion to a bool-like type to test for validity are provided.

aurora::CopiedPtr<MyClass> ptr;
if (ptr) 
     // ptr points to a valid object
if (!ptr)
     // ptr is empty (nullptr)

The member function reset() sets the pointer to a new object or to nullptr, when no argument is passed.

ptr.reset(new MyClass); // Destroys old object, assigns new object
ptr.reset();            // Destroys old object, assigns nullptr

Deep copies

The C++11 standard library and Boost don't provide any smart pointers with deep copy semantics at all. That is, you cannot copy the referenced object in general. The approach new T(*ptr) doesn't work because T might be polymorphic and you don't know the dynamic type, or there might even be no copy constructor.

aurora::CopiedPtr is able to perform deep copies. That means, the pointee (object pointed to) is copied when the pointer is copied.

aurora::CopiedPtr<MyClass> first(new MyClass);
aurora::CopiedPtr<MyClass> second = first;
// Object in first is copied to second, *first == *second

Things become more interesting when dynamic polymorphism is involved. Let's assume we have the following class hierarchy (we abandon const-correctness for simplicity):

class B
{
    public:
        virtual     ~B() {}
        virtual B*  clone() = 0;
        virtual int number() = 0;
};

class D : public B
{
     public:
         virtual D*  clone()  { return new D(*this); }
         virtual int number() { return 1; }
};

class E : public B
{
     public:
         virtual E* clone()   { return new E(*this); }
         virtual int number() { return 2; }
};

The virtual function clone() returns an identical object of the derived type. Now we declare two base smart pointers with the aurora::VirtualClone cloner to make use of the clone() function for deep copying.

aurora::CopiedPtr<B> p(new D, aurora::VirtualClone<D>());  // p->number() == 1
aurora::CopiedPtr<B> q(new E, aurora::VirtualClone<E>());  // q->number() == 2

q = p; // Perform deep copy, destroy E object. Invokes q.clone() internally.
// p->number() == 1 and q->number() == 1

The smart pointer objects have usual value semantics: We can copy and assign them, and internally the right derived object is chosen. The deep copy is safe, no object slicing occurs.

But defining this clone() function in every class in inconvenient. Forgetting it may lead to object slicing if the function has already been defined in a base class. Unfortunately, we have to define it, or how should the pointer know in which way the object is copied?

What if I told you that we actually don't? Indeed, no clone() function is required to perform deep copies across a polymorphic class hierarchy. And don't be afraid: We don't need any dynamic_cast, typeid, switch or if-else-cascade either. There is a very elegant solution encapsulated in aurora::CopiedPtr combined with the aurora::OperatorNewCopy policy.

After removing all the clone() methods from the three classes, we can write the following, just as above:

aurora::CopiedPtr<B> p, q;
p.reset(new D);  // p->number() == 1
q.reset(new E);  // q->number() == 2

q = p; // Perform deep copy, destroy E object
// p->number() == 1 and q->number() == 1

It does work. In case you wonder why: The constructor and reset() are function templates that recognize the actual type of the new-expression. By knowing the dynamic type, it is possible to invoke the corresponding copy constructor. On the other side, you can't do something like this:

aurora::CopiedPtr<B> p, q;
B* raw = new D;
p.reset(raw);
q = p;

The pointer passed to reset() is of type B* and not D*. Here, the aurora::VirtualClone policy would work again.

Advantages

CopiedPtr<T> vs. T
You might ask yourself why one wouldn't directly use T. There are several advantages of CopiedPtr<T> over T:

CopiedPtr<T> vs. T*
One can still use raw pointers to achieve some of the points mentioned above. The reasons to prefer CopiedPtr<T> are:

Limitations

Of course, aurora::CopiedPtr is not the ultimate solution to every problem. There are limitations which are useful to know: