Tags C & C++

Before C++11, the only "smart" pointer available in the standard C++ library was auto_ptr. Alas, auto_ptr isn't very smart. It has very problematic copy semantics that make it difficult to use in several important scenarios. In particular, auto_ptr can not be used with standard containers and algorithms!

To quote from Herb Sutter:

auto_ptr is most charitably characterized as a valiant attempt to create a unique_ptr before C++ had move semantics.

auto_ptr is now deprecated, and should not be used in new code. When you get a chance, try doing a global search-and-replace of auto_ptr to unique_ptr in your code base

So what is this unique_ptr thing, and what can it be used for?

Basic capabilities

To put it simply, unique_ptr should be the default smart pointer used by new C++ code, replacing "raw" pointers as much as possible. unique_ptr cleanly represents the single ownership idiom - it cannot be copied and assigned, and it cleans up the pointed object when it's destructed.

Here's some code to demonstrate this [1]:

#include <iostream>
#include <cstdlib>
#include <memory>
using namespace std;

struct Foo {
    Foo() {cerr << "Foo [" << this << "] constructed\n";}
    virtual ~Foo() {cerr << "Foo [" << this << "] destructed\n";}
};

int main(int argc, char** argv) {

    // .. some code
    {
        unique_ptr<Foo> fp(new Foo());

        unique_ptr<Foo> fp2(fp);    // ERROR! can't copy unique_ptr
        unique_ptr<Foo> fp3;
        fp3 = fp;                   // ERROR! can't assign unique_ptr

        cerr << "Exiting scope\n";
    } // fp will be destroyed, and will destruct the pointed object

    return 0;
}

The lines marked with the ERROR! comment won't actually compile. The compiler will complain saying something like:

error: use of deleted function
 'std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&)

If these two lines are commented out, the code will print:

Foo [0x845010] constructed
Exiting scope
Foo [0x845010] destructed

In addition to managing the pointed object's lifetime, unique_ptr provides the other expected capabilities of a smart pointer: it overloads operator* and operator->, provides a means to obtain the raw pointer (get), to relinquish control of the pointed object (release), and to replace the object it manages (reset). It also lets you customize the way the pointed object is deleted (if you don't want it to be the default delete operator), and has some other niceties - just consult your favorite C++ reference.

What about sources and sinks?

In this article I want to focus not on the grocery list of unique_ptr's features, but its interesting move semantics. Specifically, given that unique_ptr forbids copying and assignment, one may wonder how it can fit in the source and sink idiom which is so useful for smart pointers.

In other words, we'd like this to work:

// source creates a Foo object, wraps it in a smart pointer for safety
// and provides the result to the caller, giving it the ownership of the
// object in the process.
unique_ptr<Foo> source();

// sink gets a Foo object wrapped in a smart pointer for safety. It also
// assumes ownership of the provided object.
void sink(unique_ptr<Foo> p);

And in C++11, it does! Even though unique_ptr can't be copied, it can be moved. Move semantics are a perfect match for unique_ptr - the two concepts reinforce each other. With move semantics, unique_ptr is both safe and efficient. Here's some code to demonstrate this:

#include <iostream>
#include <cstdlib>
#include <memory>
using namespace std;

struct Foo {
    Foo() {cerr << "Foo [" << this << "] constructed\n";}
    virtual ~Foo() {cerr << "Foo [" << this << "] destructed\n";}
};

void sink(unique_ptr<Foo> p) {
    cerr << "Sink owns Foo [" << p.get() << "]\n";
}

unique_ptr<Foo> source() {
    cerr << "Creating Foo in source\n";
    return unique_ptr<Foo>(new Foo);
}

int main(int argc, char** argv) {
    cerr << "Calling source\n";
    unique_ptr<Foo> pmain = source();  // Can also be written as
                                       // auto pmain = source();

    cerr << "Now pmain owns Foo [" << pmain.get() << "]\n";
    cerr << "Passing it to sink\n";
    sink(pmain);                    // ERROR! can't copy unique_ptr
    sink(move(pmain));              // OK: can move it!

    cerr << "Main done\n";
    return 0;
}

Again, there's a line marked with ERROR! here - it demonstrates once again that a unique_ptr can't be copied. However, it can be explicitly moved, as the next line shows [2]. When the erroneous line is commented out, this code prints:

Calling source
Creating Foo in source
Foo [0x1767010] constructed
Now pmain owns Foo [0x1767010]
Passing it to sink
Sink owns Foo [0x1767010]
Foo [0x1767010] destructed
Main done

Note how cleanly the ownership is being passed between the functions in this code. At each point in time, only a single unique_ptr owns the pointed Foo object. Moreover, this is efficient - the actual pointed object only gets constructed once and destructed once.

Containers - motivation

So unique_ptr is a useful single-ownership smart pointer. But what makes it really shine (especially when compared to auto_ptr) is that it can be used in standard containers.

Why is it so important to be able to place smart pointers into containers? Because holding objects by value is sometimes very expensive. Containers, especially when coupled with algorithms, tend to move objects around. Large objects are expensive to copy, hence we'd like to keep pointers to objects inside containers instead.

What follows is a very simplistic example that demonstrates this. It shows how much more expensive it is to sort a vector of large objects that are stored by value, than it is when they're stored by pointer [3].

First, let's create a synthetic "large" object that has well defined ordering properties by some numeric ID:

struct SomeLargeData {
    SomeLargeData(int id_)
        : id(id_)
    {}
    int id;
    int arr[100];
};

We also need a function to compare two such objects. Actually, we need two - one for a container that holds object by value, and another for the by-pointer version:

bool compare_by_value(const SomeLargeData& a, const SomeLargeData& b) {
    return a.id < b.id;
}

bool compare_by_ptr(const SomeLargeData* a, const SomeLargeData* b) {
    return a->id < b->id;
}

Let's now create two vectors and populate them with random objects:

vector<SomeLargeData> vec_byval;
vector<SomeLargeData*> vec_byptr;

for (int i = 0; i < n; ++i) {
    int id = rand() % 500000;
    vec_byval.push_back(SomeLargeData(id));
    vec_byptr.push_back(new SomeLargeData(id));
}

Finally, we'll sort the two vectors with the standard sort algorithm, and measure the runtime for some large n:

sort(vec_byval.begin(), vec_byval.end(), compare_by_value);
sort(vec_byptr.begin(), vec_byptr.end(), compare_by_ptr);

The timing results I get are quite consistent - the by-pointer sorting is 2-3x faster than the by-value sorting [4]. That's a very significant difference, and it's all due to the copying sort has to do for moving the objects around inside the container.

So holding objects of non-trivial size inside standard containers is not a good idea in terms of performance. But holding raw pointers to them is also not so great, because of all the safety issues that come with raw pointers. The container can't own the pointed objects because its destructor will just "destruct" the pointer, which does nothing. So the calling code has to own the actual objects which are being shuffled around by the container. Add exceptions and/or early returns to the mix, and this is a recipe for memory leaks or even worse problems.

What we'd really like to do is let our objects be managed by a smart pointer and put that into a container. This would guarantee a clean ownership strategy - the container destroys its contents when it gets destroyed itself - just the way it should be. This is why unique_ptr is so exciting.

Containers of unique_ptr

Adapting the by-pointer version of the code above to hold unique_ptr is very simple. First, we need another comparison function:

bool compare_by_uniqptr(const unique_ptr<SomeLargeData>& a,
                        const unique_ptr<SomeLargeData>& b) {
    return a->id < b->id;
}

And then we just need to create the vector, populate it and then sort it, similarly to the way we've done for the other vectors:

vector<unique_ptr<SomeLargeData>> vec_byuniqptr;

for (int i = 0; i < n; ++i) {
    int id = rand() % 500000;
    // ...
    vec_byuniqptr.push_back(
        unique_ptr<SomeLargeData>(new SomeLargeData(id)));
}

sort(vec_byuniqptr.begin(), vec_byuniqptr.end(), compare_by_uniqptr);

That's it! And the performance? Almost identical to the by-pointer version (I measured differences of 1-5%, depending on the data).

What about shared pointers?

Another smart pointer C++11 brings with it is the shared_ptr/weak_ptr pair, implementing a reference-counted approach to shared ownership. While much more flexible than unique_ptr, shared_ptr is slower and consumes more memory; managing the reference count is not free [5].

Which one to use depends on your exact needs, but I agree with Herb Sutter's proposal of using unique_ptr by default and switching to shared_ptr if the need arises.

In addition, it is my personal opinion that preferring unique_ptr imposes a certain memory management discipline on the code, since you know at each point exactly who owns what. Shared pointers give you a sense of security you can over-use and end up with reference leaks, which are tricky to debug (just like when writing Python C extension code). Moreover, shared pointers signal the intention of APIs less clearly than owning pointers. When some factory returns a shared pointer, does it mean it keeps a reference to the object too? With an owning pointer, the API is self documenting (source returns a unique_ptr? then source is for sure giving away ownership). With a shared pointer, it does not, and need external documentation to clarify.

Conclusion

I have mentioned how rvalue references and move semantics can make code more efficient with C++11. unique_ptr is another great example that makes me want to use a C++11-capable compiler as soon as possible.

unique_ptr provides an excellent mix of efficiency and safe memory management. IMHO it's a great example of how several ideas in language design interact to create a whole that is larger than its parts.

[1]C++11 support in compilers and standard library implementations varies. To make all the code in this article work, I installed the latest gcc (4.7.1) from source on my Ubuntu box. It didn't work with 4.5, I don't know about 4.6.
[2]It can also be taken from an rvalue: sink(unique_ptr<Foo>(new Foo)) would work, because rvalue references can be moved directly.
[3]I don't claim this is good design - it's just a synthetic example created to demonstrate a point.
[4]The speedup grows as the size of the object grows. Increasing the arr member to hold 1000 integers makes the speedup 10x.
[5]For the sorting benchmark demonstrated in this article shared_ptr is about 10% slower than unique_ptr. As for size, while the size of unique_ptr is exactly the size of a raw pointer, shared_ptr is about twice as large.

Comments

comments powered by Disqus