Exploring std::unique_ptr

(1104 words)

Today we’ll talk about C++’s built-in smart pointer std::unique_ptr, which is an extremely powerful, simple & common tool.

std::unique_ptr

C++98 had std::auto_ptr. It’s problematic in areas I do not wish to discuss here, and so it was deprecated and replaced by the awesome unique_ptr.

unique_ptr is a simple ‘smart’ pointer: it holds an instance of an object and deletes it when it goes out of scope. In terms of lifetime behvior it’s much like any regular C++ object with a constructor and destructor, only with dynamic memory allocation. No reference counting, no fancy tricks.

And that’s the beauty. Simple, elegant & efficient, yet extremely powerful. With unique_ptr you no longer have to worry about new and delete. Simply call make_unique() (instead of new), and the destructor will call delete automatically. It’s as simple as that, and covers 90% of use-cases that require dynamic memory.

std::make_unique()

Above I mentioned make_unique(). This is a new standard function that came in C++14. You may think that it’s supposed to save us the need to typing the type we’re interested in, similar to make_pair. Well, not quite. Let’s look at make_unique()’s signature:

// inside namespace std
template <typename T, typename ... Args>
std::unique_ptr<T> make_unique(Args&&... args);
// '&&' here means forwarding references, not necessarily rvalues. I hope to
// explain what these are in a future post.

Note that T is not an argument to the function, thus it can’t possible be deduced by the compiler. So to use make_unique() one must always provide T explicitly, like so:

auto u_int = std::make_unique<int>(123);
cout << (*u_int == 123) << endl;
auto u_string = std::make_unique<std::string>(3, '#');
cout << (*u_string == "###") << endl;

Output:

1
1

So essentially all make_unique() does is call new and pass args as arguments to T’s constructor. As a matter of fact, here is a feature-complete implementation:

template <typename T, typename ... Args>
std::unique_ptr<T> make_unique(Args&& ... args) {
  return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
  // If you don't know what std::forward it simply read it as:
  // return std::unique_ptr<T>(new T(args...));
}

That’s it. If so, why do we even need it? What’s the difference between the following?:

auto a = std::make_unique<MyClass>();
auto b = std::unique_ptr<MyClass>(new MyClass());
std::unique_ptr<MyClass> c(new MyClass());

First difference is exception safety. In C++ < 17 calling the following can lead to a memory leak:

MyFunction(std::unique_ptr<MyClass>(new MyClass()),
           std::unique_ptr<MyClass>(new MyClass()));

How? Well, C++ doesn’t define the order of evaluation even between sub-expressions, so in theory it could evaluate the first new MyClass(), then the second new MyClass() and only then unique_ptr’s constructors. Now, if the first call to new succeeds and the second call to new throws an exception (like from MyClass’s constructor) it would leak memory as no class owns the newly created object yet.

But it’s not just exception safety. Look at the lines above, comparing the initialization of a, b and c. I think that a is the cleanest and least verbose. Furthermore, it’s the only one who doesn’t repeat MyClass twice.

And one last thing - I also prefer using make_unique() for assignment, not only for construction:

auto a = std::make_unique<int>(123);
a = std::make_unique<int>(456);  // Cool.
a.reset(new int(789));  // Works, but not as nice.

How to use unique_ptr

Use unique_ptr to represent any owned object that’s not shared. Here are some common examples:

Return a dynamically allocated object from a function

std::unique_ptr<MyObject> CreateMyObject();

This way whoever calls your function won’t leak. Even if they don’t assign the returned value to a variable:

SomeFunction();
CreateMyObject();  // no assignment, yet no leak
SomeOtherFunction();

Take ownership of a dynamically-allocated object

void TakeOwnership(std::unique_ptr<AnObject> obj);

With such signature callers can’t ignore the fact that you’re taking ownership of obj:

TakeOwnership(3);  // ERROR: no conversion from 'int' to 'std::unique_ptr<int>'

int* p = new int(3);
TakeOwnership(p);  // ERROR: no conversion from 'int*' to 'std::unique_ptr<int>'
                   // This is because unique_ptr's constructor is explicit.

auto u = std::make_unique<int>(3);
TakeOwnership(u);  // ERROR: no copy constructor

TakeOwnership(std::move(u));  // OK
TakeOwnership(std::make_unique<int>(3));  // OK
TakeOwnership(nullptr);  // OK -- due to constructor accepting nullptr_t

As you may have noticed, unique_ptr supports C++11’s move semantics, but does not allow copying. This makes sense – as the name suggests, each instance is supposed to only track a unique instance.

Dynamically-allocated class members

Use unique_ptr to automatically release class members when a class is released:

class SomeObject {
  // No destructor needed

private:
  std::unique_ptr<SomethingElse> m_SomethingElse;
};

Casting

unique_ptr supports construction of unique_ptr<T> from unique_ptr<U> if T* is convertible to U* (which usually means up-casting). Example:

struct Base { virtual ~Base() = default; };
struct Derived : Base {};

// ...
std::unique_ptr<Derived> derived = std::make_unique<Derived>();
std::unique_ptr<Base> base(std::move(derived));

Note that we must call std::move() on derived – that’s because there can’t be 2 instances of unique_ptr pointing to the same object, even if they are of different types.

Custom deleter

unique_ptr allows to specify a custom object that will be used for releasing the object. To use this, however, one must provide a second template argument. For example:

FILE* file = fopen("...", "r");
auto FILE_releaser = [](FILE* f) { fclose(f); };
std::unique_ptr<FILE, decltype(FILE_releaser)> file_ptr(file, FILE_releaser);

As demonstrated above, this can be very useful when working with C APIs, or APIs which have custom release logic.

Notes:

Misusing unique_ptr

If you try hard, you could do some nasty things with unique_ptr. But you have to put some effort to do so. Here are a few examples:

Assigning the same pointer to multiple unique_ptrs

int* p = new int(123);
std::unique_ptr<int> a(p);
std::unique_ptr<int> b(p);  // Oops - b's destructor will double delete p

The above example can be avoided by always using make_unique() instead of calling new directly.

Here’s a similar example, but done without calling new directly:

std::unique_ptr<int> a = std::make_unique<int>(123);
std::unique_ptr<int> b(a.get());

Using unique_ptr’s constructor directly is not recommended, and passing the pointer returned by .get() to a function that is taking ownership is an error.

Deleting memory managed by unique_ptrs

auto u = std::make_unique<int>(123);
delete u.get();

This is an example of why you:

A word about arrays

unique_ptr also have partial specializaion to handle arrays. Specifically it calls delete[] rather than delete. However, using C-style arrays is something that should generally be avoided. Prefer std::array or std::vector where possible.

That’s it for today

In the next post we’ll talk about std::shared_ptrunique_ptr’s brother which is very interesting, however less frequently used.


Comments