Follow the arrows

When implementing our smart pointers, unique_ptr and shared_ptr, we glossed over the methods operator* and operator->. Now it’s time to see how they work! Let’s look at operator* first:

T& operator*()
{
    return *internal_pointer;
}

When we dereference one of our smart pointers with *, the above method is called. This dereferences the internal_pointer and returns the result. So with our overload we are reaching through our internal pointer to the underlying object. When you dereference a pointer of type T* you get something of type T, so why is the return type T&? Well, the & here means that we are returning by reference. We do this, because we do not want to copy the underlying object when we return it. Suppose the return type was actually T. Then, when we dereferenced a smart pointer, the object pointed to by the internal pointer would be copied to the point where we dereferenced and we would be accessing the members of the copy not the original.

Now let’s look at the -> operator. We defined it like this:

T* operator->()
{
    return internal_pointer;
}

Notice, now we do not dereference, instead we return the internal pointer directly. Suppose we have a simple class like this:

class SillyClass
{
public:
    void SayHello()
    {
        std::cout << "Hello" << std::endl;
    }
};

and in our main method we have created a unique_ptr to a SillyClass object, and use the -> operator to call it’s SayHello method. Like this:

int main()
{
    unique_ptr<SillyClass> ptr(new SillyClass());
    ptr->SayHello();
}

Normally we think of the operation ptr->SayHello() as being equivalent to (*ptr).SayHello(). However, what it does is slightly more complicated.

The -> operator works recursively. If it is called on a pointer type, it dereferences the pointer and resolves the name you asked for, in this case, SayHello. If it is called on a non pointer type, it calls the operator-> method for that type and continues recursively from there.

So in our case, as ptr is not a pointer type, the method unique_ptr::operator-> is called, and the -> operator is applied to the return value. The return value here is of type SillyClass*, this is a pointer type, so it is dereferenced and it’s method SayHello is resolved.

We can see this recursive strategy working in a, very contrived, example. Suppose we wrote the following classes:

class BottomLevel
{
public:
    void SayHello()
    {
        std::cout << "Hello" << std::endl;
    }
};

class MidLevel
{
private:
    BottomLevel* bottomLevel;    
public:
    MidLevel() : bottomLevel(new BottomLevel())
    {
    }

    BottomLevel* operator->()
    {
        return bottomLevel;
    }
};

class TopLevel
{
private:
    MidLevel midLevel;

public:
    TopLevel() : midLevel()
    {
    }

    MidLevel operator->()
    {
        return midLevel;
    }
};

And then we had a main method like this:

int main()
{
    TopLevel top;
    top->SayHello();
}

Here, top is a non pointer type, so -> calls the operator-> method on the top object. This returns a MidLevel object. Midlevel is also not a pointer, so the operator-> method on that object gets called. That returns a pointer to an object of type BottomLevel. As this is a pointer, it is dereferenced, and the method SayHello on this object is resolved.

In practice you usually can just think of the arrow operator as being equivalent to dereferencing and then resolving the name on the underlying type, but when implementing smart pointers, you need to get this right.

Sharing is fun!

So, we have already seen how unique pointers work. They wrap a raw pointer, they cannot be copied and they delete their internal pointer when the they go out of scope. But sometimes we want to use RAII with our pointers, but we need more than a single copy of them. That’s where shared pointer comes in. A shared pointer is just like a unique pointer, except it can be copied! So we can have multiple shared pointers pointing to the same underlying data.

The really clever thing is that a shared pointer keeps track of how many copies of itself there are. It does this by maintaining a counter of the number of copies, called the reference count. When all of the copies of a given shared pointer have gone out scope, the reference count will be 0 and delete is called on the underlying raw pointer. So even though there are multiple copies of the same shared pointer, we can still be sure that the underlying raw pointer will get deleted. You can (sort of) think of a unique pointer as a shared pointer, with a maximum reference count of 1.

Ok then, let’s look at some code!

template<typename T>
class shared_ptr
{
private:
    T* internal_pointer;
    uint* reference_count;

public:
    shared_ptr(T* internal_pointer) : internal_pointer(internal_pointer)
    {
        reference_count = new uint(1);
    }

    shared_ptr(const shared_ptr& source) : internal_pointer(source.internal_pointer), reference_count(source.reference_count)
    {
        (*reference_count)++;
    }

    shared_ptr<T>& operator=(const shared_ptr& source)
    {
        on_reference_deletion();
        internal_pointer = source.internal_pointer;
        reference_count = source.reference_count;
        (*reference_count)++;
        return *this;
    }

    shared_ptr(shared_ptr&& source) : internal_pointer(source.internal_pointer)
, reference_count(source.reference_count)
    {
        source.reference_count = nullptr;
        source.internal_pointer = nullptr;
    }

    shared_ptr<T>& operator=(shared_ptr&& source)
    {
        on_reference_deletion();
        internal_pointer = source.internal_pointer;
        reference_count = source.reference_count;
        source.internal_pointer = nullptr;
        source.reference_count = nullptr;
        return *this;
    }

    T& operator*()
    {
        return *internal_pointer;
    }

    T* operator->()
    {
        return internal_pointer;
    }

    ~shared_ptr()
    {
        if(reference_count)
        {
            on_reference_deletion();
        }
    }

private:
    void on_reference_deletion()
    {
        (*reference_count)--;
        if(*reference_count == 0)
        {
            delete internal_pointer;
        }
    }
};

This is of course very similar to our implementation of unique_ptr. However, now we have an extra member, a pointer to an unsigned integer called reference_count. This is the counter that keeps track of how many copies of this shared_ptr there are. It is a pointer so that all the distinct copies can share the same counter. In the constructor,

    shared_ptr(T* internal_pointer) : internal_pointer(internal_pointer)
    {
        reference_count = new uint(1);
    }

we are creating the first instance of this shared_ptr so we set the reference counter to 1.

We also have a copy constructor now:

    shared_ptr(const shared_ptr& source) : internal_pointer(source.internal_pointer), reference_count(source.reference_count)
    {
        (*reference_count)++;
    }

In this constructor, we copy the internal_pointer and the reference_count from source. Then, as there is a new copy of this shared_ptr, we increment the the reference counter.

The move constructor is similar to the one in unique_ptr:

    shared_ptr(shared_ptr&& source) : internal_pointer(source.internal_pointer), reference_count(source.reference_count)
    {
        source.reference_count = nullptr;
        source.internal_pointer = nullptr;
    }

First we copy across the internal_pointer and the reference_count. We have moved out of source, so it should no longer have any reference to the underlying data. So we set the two pointers inside of source to nullptr. This means that source can now go out of scope, without effecting the internal_pointer or reference_count. Moving does not increase the number of references, so we don’t need to increment the reference counter here.

The copy constructor and move assignment are a little bit more complicated. An object that is being copy or move assigned, will already have been initialised. That means that it already has a pointer to some data and a reference count. So, before the assignment can happen, it needs to decrement the reference count, and if necessary delete the data pointed to by internal_pointer. This operation is handled by the function on_reference_deletion:

    void on_reference_deletion()
    {
        (*reference_count)--;
        if(*reference_count == 0)
        {
            delete internal_pointer;
        }
    }

So our copy assignment looks like this:

    shared_ptr<T>& operator=(const shared_ptr& source)
    {
        on_reference_deletion();
        internal_pointer = source.internal_pointer;
        reference_count = source.reference_count;
        (*reference_count)++;
        return *this;
    }

First we call on_reference_deletion. Then we copy over the data from the source shared_ptr, both the internal_pointer and the reference_count. Then, as we have made a new copy of the source shared_ptr, we increment the reference count.

Then there is the the move assignment operator:

    shared_ptr<T>& operator=(shared_ptr&& source)
    {
        on_reference_deletion();
        internal_pointer = source.internal_pointer;
        reference_count = source.reference_count;
        source.internal_pointer = nullptr;
        source.reference_count = nullptr;
        return *this;
    }

As before, first we call on_reference_deletion and then we copy across the two internal pointers. Just like in the move constructor, we do not want source to have pointer to our data, so we set both it’s internal pointers to nullptr. Again, as moving does not increase the number of references, we do not have to increment the reference count.

An important thing to note, is that, when we move out of a shared_ptr it is not left in a good state. It’s pointers are both set to nullptr. If we try to access them we will get undefined behaviour. This is what we would expect, when we move, we are transferring ownership, so we don’t expect the source to be usable anymore.

Finally, let’s look at our destructor:

    ~shared_ptr()
    {
        if(reference_count)
        {
            on_reference_deletion();
        }
    }

Here, we hare destroying a shared_ptr, so we call on_reference_deletion. However, it is possible that this a shared_ptr that was moved. So first we check to see that the reference_count is a valid pointer. If it is not, we don’t need to do anything.

The great thing about shared_ptr is that we can keep multiple pointers to the same underlying data, and pass them around without having to worry about freeing that memory. It’s almost like a using a language with a garbage collector, like C# or Java. Indeed, shared pointers are a little bit better than garbage collection. In C# or Java, we know only that the garbage collector will reclaim unused memory at some point after the last reference goes out of scope. However, with shared pointers, the memory is reclaimed exactly when the last reference goes out of scope! Which is much more efficient. The one downside compared to garbage collection is that shared_ptr cannot handle circular references. But, we can just be very careful and not create any of them.

auto_ptr is dead, long live unique_ptr!

We talked a bit about auto_ptr in a previous post. In particular we talked about the problem with copying auto_ptr. The auto_ptr class had a copy constructor and copy assignment. However as it was supposed to represent a single unique pointer, these were not actually copies at all. This would cause issues with algorithms that relied on the ability to make normal copies.

So, when C+11 was introduced, a new feature premiered, move semantics. We’ve talked a lot about move semantics already. A move is like a copy, but, you transfer ownership of a resource rather than copying it. That means that the source of the move, is not necessarily left in it’s original state.

Move semantics are just what was needed to fix auto_ptr, and that is how unique_ptr was born. The code looks something like this:

template<typename T>
class unique_ptr
{
private:
    T* internal_pointer;
public:
    unique_ptr(T* internal_pointer) : internal_pointer(internal_pointer)
    {
    }

    unique_ptr(unique_ptr& rhs) = delete;

    unique_ptr<T>& operator=(unique_ptr& rhs) = delete;

    unique_ptr(unique_ptr&& rhs) : internal_pointer(rhs.internal_pointer)
    {
        rhs.internal_pointer = nullptr;
    }

   unique_ptr<T>& operator=(unique_ptr&& rhs)
   {
      delete internal_pointer;
      internal_pointer = rhs.internal_pointer;
      rhs.internal_pointer = nullptr;
      return *this;
   }

   T& operator*()
   {
       return *internal_pointer;
   }

   T* operator->()
   {
       return internal_pointer;
   }

   ~unique_ptr()
   {
       delete internal_pointer;
    }
};

As you can see, this is very similar to our implementation of auto_ptr. There is one big difference. Instead of fake copies, we are using bona fide moves. Indeed, we don’t want to allow copies at all, so we delete the copy constructor and copy assignment explicitly:

   unique_ptr(unique_ptr& rhs) = delete;

   unique_ptr<T>& operator=(unique_ptr& rhs) = delete;

Now, if someone tries to copy a unique_ptr object, their code will not compile. To allow a unique_ptr object to be passed around, we have implemented a move constructor and move assignment:

   unique_ptr(unique_ptr&& rhs) : internal_pointer(rhs.internal_pointer)
   {
       rhs.internal_pointer = nullptr;
   }

   unique_ptr<T>& operator=(unique_ptr&& rhs)
   {
        delete internal_pointer;
        internal_pointer = rhs.internal_pointer;
        rhs.internal_pointer = nullptr;
        return *this;
   }

These have the same implementations as our copy constructor and copy assignment in auto_ptr. However, the && in the signature of these functions means they will only ever be called for a move, not a copy.

The move constructor and assignment will be used when we are constructor or assigning from an object that the compiler can see is just a temporary object. We can also use std::move to tell the compiler explicitly to move our unique pointer. For example, when we passed an auto_ptr to a function, it would have been copied, but with unique_ptr we can move it, like this:

class SomeClass
{
    // pretend there is some interesting code here
}

template<typename T>
void SomeFunction(unique_ptr<T> ptr)
{
    // imagine we do something with ptr here
}

int main()
{
    unique_ptr<SomeClass> ptr(new SomeClass());
    SomeFunction(std::move(ptr));
}

So we now have a safe way to manage a raw pointer. By using unique_ptr we can be sure that delete will be called for our raw pointer when the scope is unwound, we have a guarantee that the unique_ptr will not be copied, and we can move it when we need to.

The Mystery of the Missing Pointer

What was auto_ptr?

The class auto_ptr added smart pointer functionality to C++. It implemented a simple form of RAII. It did this by wrapping a normal (raw) pointer, and calling delete on the pointer in it’s destructor. So, the typical programmer (whether stubborn or not) could use it to manage a raw pointer, and be sure that delete would be called on the raw pointer when the auto_ptr went out of scope.

The implementation looked a little something like this:

template<typename T>
class auto_ptr 
{
private:
    T* internal_pointer;
public:
    auto_ptr(T* internal_pointer) : internal_pointer(internal_pointer)
    {
    }

    auto_ptr(auto_ptr& rhs) : internal_pointer(rhs.internal_pointer)
    {
        rhs.internal_pointer = nullptr;
    }

    auto_ptr<T>& operator=(auto_ptr& rhs)
    {
        delete internal_pointer;
        internal_pointer = rhs.internal_pointer;
        rhs.internal_pointer = nullptr;
        return *this;
    }

    T& operator*()
    {
        return *internal_pointer;
    }

    T* operator->()
    {
        return internal_pointer;
    }

    ~auto_ptr()
    {
        delete internal_pointer;
    }
}

The overloads for the operators operator* and operator-> allow us to treat an auto_ptr as if it were a raw pointer of type T. We can dereference straight to the members of the object of type T that the internal pointer points to. How this actually works is slightly subtle, but the important point is that it works!

This seems like a pretty useful class. So it might surprise you to learn that auto_ptr was deprecated in C++11 and then removed completely in C++17. Usually, there needs to be a pretty good reason to remove a feature like this. So why was it done?

Well, it turns out, that auto_ptr had a pretty huge flaw. Also a separate new feature was added to C++11 that meant a different, better implementation for this kind of smart pointer could be added.

What Was Wrong with auto_ptr?

An important point to understand is that we can only ever have one auto_ptr for a given raw pointer. Suppose we had two such auto_ptrs, when one of them went out of scope, it would delete the underlying memory, but the other auto_ptr would still have a raw pointer to that memory. Which could cause a use after delete or double delete. Which are both undefined behaviour in C++.

So we can’t really copy a auto_ptr. The copy constructor and assignment for auto_ptr could have been removed. This was done in C++ version before 11 by declaring the methods as private, and not giving an implementation. However, we want to be able to pass the auto_ptr into functions and to return it, which uses copying. So, we do a have a copy constructor and copy assignment. Let’s take a look at them:

    auto_ptr(auto_ptr& rhs) : internal_pointer(rhs.internal_pointer)
    {
        rhs.internal_pointer = nullptr;
    }

    auto_ptr<T>& operator=(auto_ptr& rhs)
    {
        delete internal_pointer;
        internal_pointer = rhs.internal_pointer;
        rhs.internal_pointer = nullptr;
        return *this;
    }

These methods copy the internal pointer of rhs, and then set the value of the internal pointer in rhs to nullptr. In the copy assignment we also have to delete the internal pointer, as the object it points to is going out of scope. Neither of these are a copy in any meaningful sense of the world. When copying something, we would not expect the object we are copying to be modified. We would expect to end up with two identical copies of the original object. What this actually does is transfer ownership of the underlying pointer.

This was a problem. We’ve claimed to implement copy, but have done something entirely different. Imagine a photocopier, that produced a perfect copy of whatever document you fed it, but then shredded the original! This caused a particular problem with the standard containers like vector and standard algorithms like sort. The behaviour of these containers and algorithms relied on a normal non-destructive implementation of copy. It was possible to compile code that used auto_ptr with these containers and algorithms, but the resulting behaviour could be very bad!

What Replaced auto_ptr?

What we were trying to do in the copy constructor and copy assignment in auto_ptr was really a move, not a copy. With C++11 move semantics were built right into the language. This meant that a new type of smart pointer could be added, that managed a raw pointer and moved it rather than copying it. This new smart pointer was called unique_ptr. The standard containers and algorithms were also modified in C++11 to use move semantics, so it is possible to use them with unique_ptr.