Pointer Implications (C++11)

By | 2013-08-07

While C++98 had smart pointers of a sort; they weren’t good enough for use. The language lacked the key feature that makes smart pointers work: move semantics. C++11 has introduced that language feature, and hence has solid smart pointers.

The addition of smart pointers gives us some further options for passing references to objects when we’re using C++11. These give us a way of expressing more specifically what a function wants to do with a pointer.

  • f( unique_ptr<O> )
  • f( shared_ptr<O> )

And some weirdos:

  • f( unique_ptr<O> * )
  • f( unique_ptr<O> & )
  • f( const unique_ptr<O> & )
  • f( shared_ptr<O> * )
  • f( shared_ptr<O> & )
  • f( const shared_ptr<O> & )

f(unique_ptr<O>)

unique_ptr is aptly named: it is a pointer that there can be only one of. There is no way to copy a unique_ptr<>, so the following will be disallowed by the compiler:

    void f(unique_ptr<O> p) {
        // ... see below ...
    }

    // ... elsewhere in the forest ...
    unique_ptr<O> myObject = new myObject;
    // ERROR: No copying
    unique_ptr<O> copyObject = myObject;
    // ERROR: No copying, even for function parameters
    f(myObject);

That means that f(unique_ptr<O>) is not like f(O*), it’s saying more than just “f(p) modifies p”. The only way to call this f() is by giving up ownership of the passed object, so that there is still only one unique copy of the pointer. Our f() declaration therefore tells the caller “f(p) takes ownership of p and you need to explicitly give that ownership”. How do we do that? With move semantics. There is no unique_ptr<> copy constructor, but there is a move constructor.

    void f(unique_ptr<O> p) {
        // ... see below ...
    }

    // ... elsewhere in the forest ...
    unique_ptr<O> myObject = new myObject;
    f(std::move(myObject));
    // ERROR: myObject is implicitly now a null unique_ptr<>
    myObject->modify();

f() here is promising that it will manage the lifetime of the given object (unique_ptr<> will, of course, free the underlying resource when it goes out of scope). We can therefore guess the form of f() to a certain degree.

    void f(unique_ptr<O> p) {
        if(p)
            someGlobalContainer.push_back(std::move(p));
    }

Of particular note: unique_ptr<> as a parameter has pointer semantics, not reference semantics. That is to say that it can be null, or rather the unique_ptr<> representation of null. It includes an operator bool()(), which is automatically called in conditional contexts (like if(p)), to allow you to check for that null representation. Importantly: you must check for it, just as you would check a pointer parameter was not nullptr before you used it.

Other than that, we can see that once f() has completed, p’s lifetime is no longer the concern of the caller. We’ve explicitly said that f() takes ownership. f() implicitly tells the caller that it must pass an object that has long lifetime – i.e. heap storage rather than automatic storage; that’s pretty much guaranteed by use of a unique_ptr<>, which is very hard to trick into taking ownership of automatic storage – and if you do that then you’re already seriously broken.

A quick aside to mention the converse case is just as useful in C++11: when a function generates a new object, it should be returned as a unique_ptr<> too.

    unique_ptr<O> g() {
        unique_ptr<O> ret = new O;
        ret->configure();

        // C++11 will automatically use move semantics for a returned
        // automatic-storage variable
        return ret;
    }

f(shared_ptr<O>)

It should be getting easy for you now. Unlike in the unique_ptr<> case, the caller gets to keep a copy of the pointer. You can copy a shared_ptr<>. Be aware that a shared_ptr<> supplies that facility at non-zero cost. Reference counts have to be maintained, and they have to be atomic, so imply some degree of locking or memory barrier use. Both of which can have performance implications.

That means the following is not illegal:

    void f(shared_ptr<O> p) {
        // ... stores a copy ...
    }

    // ... elsewhere in the forest ...
    shared_ptr<O> myObject = new myObject;
    f(myObject);
    // myObject is still valid
    myObject->modify();
    // When our shared_ptr<O> goes out of scope, the object is not
    // necessarilly deleted

You have to be a little careful because shared_ptr<> means you should only control the object lifetime through shared_ptr<>, this combined with the ability to pass unwrapped pointers to a shared_ptr<> constructor means you must be careful not to delete the resource yourself.

    void f(shared_ptr<O> p) {
        // ... stores a copy ...
    }

    // ... elsewhere in the forest ...
    O *myObject = new myObject;
    f(myObject);
    delete myObject;

Weirdos

  • f( unique_ptr<O> * )
  • f( unique_ptr<O> & )
  • f( shared_ptr<O> * )
  • f( shared_ptr<O> & )

Passing a reference to a smart pointer says “this function will change what your smart pointer refers to”. The above are not the way to do that. If you have a function that wants to replace a smart pointer, you should be using move semantics and this form:

// Used like this: x = f(std::move(x));
unique_ptr<O> f(unique_ptr<O> p) {
   // replace
   p = unique_ptr<O>( new O );
   // return
   return p;
}

Which more accurately expresses what you’re doing – the caller has to explicitly move() their unique_ptr<> to f() as above, and will be given a new pointer back. C++11 will force the use of std::move() for the parameter and will automatically use move semantics to get the replacement unique_ptr<> back out.

  • f( const unique_ptr<O> & )
  • f( const shared_ptr<O> & )

Splutter. The function wants to be given control over a smart pointer it’s not going to modify? Nonsense. There is no reason you’d do either of these.

Conclusions

Here then is how you should use each of these parameter variations:

f(O&)
“Modifies an O, which must exist. Does not delete the O. Does not take a copy of O.”
f(O*)
Don’t use it. Use f(O&).
f(const O&)
“Queries an O, which must exist.”
f(const O*)
“Queries an O, which might not exist. Provides fallback or exception in the event that the O does not exist”.
f(unique_ptr<O>)
“Takes ownership of an O, if it exists. The caller must explicitly give ownership – meaning it loses it”.
f(shared_ptr<O>)
“Takes shared ownership of an O, if it exists. The caller need not explicitly give ownership; however if it is not explicit it must not arbitrarily delete the now-shared O”.

Leave a Reply