Menu

Eng: Modern C++/unique_ptr

Next up in the whirlwind tour: std::unique_ptr. This is tremendously useful in that it lets us get 100% of the performance of a C pointer with effectively zero chance of leaking an allocated resource.

I’m going to forego the usual example of resource leaks. I’m going to assume you know how they’re possible, and I’m going to underline something else: in C++ it’s possible for them to happen even if you think you’ve got a single exit point for your function and you’ve got cleanup code there. If your function calls foo(), and foo() happens to throw an exception which you don’t know about and don’t catch, your function will stop execution where it invokes foo() and will unwind from there — bypassing your carefully guarded memory management block!

But what the C++ specification taketh away in the form of nondeterministic function exit (well, kind of: it’s deterministic, it just requires you to understand the exception-throwing behavior of literally everything beneath you in the call stack, which is just unreasonable), it giveth in the form of object destructors. C++ guarantees that any stack-allocated object will have its destructor called upon exit from that stack frame, no matter how it’s done.

In 1998 the Committee put on their thinking cap. “What if we create a special object that contains a pointer to a dynamically-allocated object? And we could use our special object like it was a pointer, except that as soon as its stack frame exited our special object’s destructor would always free the dynamically-allocated object it was safeguarding?”

Welcome to 1998 and std::auto_ptr. It was a terrible mess for reasons too long to go into here.

(The short version is C++ containers, back in 1998, never took possession of objects by reference or pointer; instead, if you passed an integer into a container, a new integer was allocated within the container and was assigned the value of the integer you gave it. This meant containers were very safe, since they only operated on copies of data and never the actual data itself. This also meant ownership semantics of std::auto_ptrs got very, very weird: if you create two which pointed to the same dynamically allocated object, which one should be responsible for cleanup? As a general rule, std::auto_ptrs could not be safely used with C++ containers. This was a hideous oversight that deeply embarrassed the Committee, and they’ve since remedied their error with std::unique_ptr.)

There is a subtlety with it, though, and it stems from how C++ handles temporary variables. Let’s say for sake of argument you have a function that takes two std::unique_ptrs as parameters.

void foo(unique_ptr<U> bar, unique_ptr<U> baz);

And let’s say you call it like such:

foo(unique_ptr<U>(new U()), unique_ptr<U>(new U()));

Congratulations: you just screwed up. C++ does not specify the order in which parameters get evaluated, so it’s possible for your first call to new U() to execute successfully and a temporary variable containing a pointer to the new U object is created; then the second call to new U() throws an exception. At this point your unique_ptrs haven’t been created, which means when the stack frame unwinds your first U() is leaked: it’s a dangling resource!

Fortunately, C++ provides a tool that avoids this: std::make_unique. Now that we have std::unique_ptrs to play with, you need to adopt this simple rule of thumb: never, never, never, ever, use the new keyword. You don’t need to. Use std::make_unique instead. E.g.:

foo(make_unique<U>(), make_unique<U>());

This is guaranteed to never leak a resource, even if the second allocation throws an exception.

Let’s put it into practice: we’re going to create an array of three dynamically-generated objects of different classes (but a common ancestor), and then throw some exception to force the stack to unwind. If the objects are correctly deallocated, they’ll print messages to the console error announcing they’ve been harvested.

Try it yourself

destroying a Shapedestroying a Squaredestroying a Circlelibc++abi.dylib: terminating with uncaught exception of type intAbort trap: 6

See? Easy enough. Using the new tools isn’t all that hard, even if understanding the reasons behind them involves a little bit of head-bending.

But trust me. Throw away new, delete, and raw pointers. Wherever you’d previously use a raw pointer, use std::unique_ptr, and always create them using the std::make_unique family of convenience functions.

3 Comments

(Note: in the above code, I both listed “using std::array” and then defined it in main() as a “std::array”, forgetting that I could’ve just made it “array”. This is not an error, although it is bad style. I’ll fix the image when I have a moment…)