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.

Leave a Reply

Your email address will not be published. Required fields are marked *