Tip of the Week #126: `make_unique` is the new `new`

Originally posted as totw/126 on 2016-12-12

By James Dennett ([email protected]) based on a mailing list post by Titus Winters ([email protected])

As a codebase expands it is increasingly difficult to know the details of everything you depend on. Requiring deep knowledge doesn’t scale: we have to rely on interfaces and contracts to know that code is correct, both when writing and when reviewing. In many cases the type system can provide those contracts in a common fashion. Consistent use of type system contracts makes for easier authoring and reviewing of code by identifying places where there are potentially risky allocations or ownership transfers for objects allocated on the heap.

While in C++ we can reduce the need for dynamic memory allocation by using plain values, sometimes we need objects to outlive their scope. C++ code should prefer smart pointers (most commonly std::unique_ptr) instead of raw pointers when dynamically allocating objects. This provides a consistent story around allocation and ownership transfer, and leaves a clearer visual signal where there’s code that needs closer inspection for ownership issues. The side effect of matching how allocation works in the outside world post-C++14 and being exception safe is just icing.

Two key tools for this are absl::make_unique() (a C++11 implementation of C++14’s std::make_unique(), for leak-free dynamic allocation) and absl::WrapUnique() (for wrapping owned raw pointers into the corresponding std::unique_ptr types). They can be found in absl/memory/memory.h.

Why Avoid new?

Why should code prefer smart pointers and these allocation functions over raw pointers and new?

  1. When possible, ownership is best expressed in the type system. This allows reviewers to verify correctness (absence of leaks and of double-deletes) almost entirely by local inspection. (In code that is exceptionally performance sensitive, this may be excused: while cheap, passing std::unique_ptr across function boundaries by value has non-zero overhead because of ABI constraints. That’s rarely important enough to justify avoiding it.)

  2. Somewhat like the reasoning for preferring push_back() over emplace_back() (TotW 112), absl::make_unique() directly expresses the intent and can only do one thing (do the allocation with a public constructor, returning a std::unique_ptr of the specified type). There’s no type conversion or hidden behavior. absl::make_unique() does what it says on the tin.

  3. The same could be achieved with std::unique_ptr<T> my_t(new T(args)); but that is redundant (repeating the type name T) and for some people there’s value in minimizing calls to new. More on this in #5.

  4. If all allocations are handled via absl::make_unique() or factory calls, that leaves absl::WrapUnique() for the implementation of those factory calls, for code interacting with legacy methods that don’t rely on std::unique_ptr for ownership transfer, and for rare cases that need to dynamically allocate with aggregate initialization (absl::WrapUnique(new MyStruct{3.141, "pi"})). In code review it’s easy to spot the absl::WrapUnique calls and evaluate “does that expression look like an ownership transfer?” Usually it’s obvious (for example, it’s some factory function). When it’s not obvious, we need to check the function to be sure that it’s actually a raw-pointer ownership transfer.

  5. If we instead rely mostly on the constructors of std::unique_ptr, we see calls like: \ std::unique_ptr<T> foo(Blah()); \ std::unique_ptr<T> bar(new T()); \ It takes only a moment’s inspection to see that the latter is safe (no leak, no double-delete). The former? It depends: if Blah() is returning a std::unique_ptr, it’s fine, though in that case it would be more obviously safe if written as \ std::unique_ptr<T> foo = Blah(); \ If Blah() is returning an ownership-transferred raw pointer, that’s also fine. If Blah() is returning just some random pointer (no transfer), then there’s a problem. Reliance on absl::make_unique() and absl::WrapUnique() (avoiding the constructors) provides an additional visual clue for the places where we have to worry (calls to absl::WrapUnique(), and only those).

How Should We Choose Which to Use?

  1. By default, use absl::make_unique() (or std::make_shared() for the rare cases where shared ownership is appropriate) for dynamic allocation. For example, instead of: std::unique_ptr<T> bar(new T()); write auto bar = absl::make_unique<T>(); and instead of bar.reset(new T()); write bar = absl::make_unique<T>();

  2. In a factory function that uses a non-public constructor, return a std::unique_ptr<T> and use absl::WrapUnique(new T(...)) in the implementation.

  3. When dynamically allocating an object that requires brace initialization (typically a struct, an array, or a container), use absl::WrapUnique(new T{...}).

  4. When calling a legacy API that accepts ownership via a T*, either allocate the object in advance with absl::make_unique and call ptr.release() in the call, or use new directly in the function argument.

  5. When calling a legacy API that returns ownership via a T*, immediately construct a smart pointer with WrapUnique (unless you’re immediately passing the pointer to another legacy API that accepts ownership via a T*).

Summary

Prefer absl::make_unique() over absl::WrapUnique(), and prefer absl::WrapUnique() over raw new.


Subscribe to the Abseil Blog