Managing dynamic memory allocations in C++
C++ supports dynamic allocation of objects using new
. For new
objects to not leak, they must be delete
d. This is quite difficult to do correctly in complex code. Smart pointers are the canonical solution. Mozilla has historically used nsAutoPtr
, and C++98 provided std::auto_ptr
, to manage singly-owned new
objects. But nsAutoPtr
and std::auto_ptr
have a bug: they can be “copied.”
The following code allocates an int
. When is that int
destroyed? Does destroying ptr1
or ptr2
handle the task? What does ptr1
contain after ptr2
‘s gone out of scope?
typedef auto_ptr<int> auto_int; { auto_int ptr1(new int(17)); { auto_int ptr2 = ptr1; // destroy ptr2 } // destroy ptr1 }
Copying or assigning an auto_ptr
implicitly moves the new
object, mutating the input. When ptr2 = ptr1
happens, ptr1
is set to nullptr
and ptr2
has a pointer to the allocated int
. When ptr2
goes out of scope, it destroys the allocated int
. ptr1
is nullptr
when it goes out of scope, so destroying it does nothing.
Fixing auto_ptr
Implicit-move semantics are safe but very unclear. And because these operations mutate their input, they can’t take a const
reference. For example, auto_ptr
has an auto_ptr::auto_ptr(auto_ptr&)
constructor but not an auto_ptr::auto_ptr(const auto_ptr&)
copy constructor. This breaks algorithms requiring copyability.
We can solve these problems with a smart pointer that prohibits copying/assignment unless the input is a temporary value. (C++11 calls these rvalue references, but I’ll use “temporary value” for readability.) If the input’s a temporary value, we can move the resource out of it without disrupting anyone else’s view of it: as a temporary it’ll die before anyone could observe it. (The rvalue reference concept is incredibly subtle. Read that article series a dozen times, and maybe you’ll understand half of it. I’ve spent multiple full days digesting it and still won’t claim full understanding.)
Presenting mozilla::UniquePtr
I’ve implemented mozilla::UniquePtr
in #include "mozilla/UniquePtr.h"
to fit the bill. It’s based on C++11’s std::unique_ptr
(not always available right now). UniquePtr
provides auto_ptr
‘s safety while providing movability but not copyability.
UniquePtr
template parameters
Using UniquePtr
requires the type being owned and what will ultimately be done to generically delete it. The type is the first template argument; the deleter is the (optional) second. The default deleter performs delete
for non-array types and delete[]
for array types. (This latter improves upon auto_ptr
and nsAutoPtr
[and the derivative nsAutoArrayPtr
], which fail horribly when used with new[]
.)
UniquePtr<int> i1(new int(8)); UniquePtr<int[]> arr1(new int[17]());
Deleters are callable values, that are called whenever a UniquePtr
‘s object should be destroyed. If a custom deleter is used, it’s a really good idea for it to be empty (per mozilla::IsEmpty<D>
) so that UniquePtr<T, D>
is as space-efficient as a raw pointer.
struct FreePolicy { void operator()(void* ptr) { free(ptr); } }; { void* m = malloc(4096); UniquePtr<void, FreePolicy> mem(m); int* i = static_cast<int*>(malloc(sizeof(int))); UniquePtr<int, FreePolicy> integer(i); // integer.getDeleter()(i) is called // mem.getDeleter()(m) is called }
Basic UniquePtr
construction and assignment
As you’d expect, no-argument construction initializes to nullptr
, a single pointer initializes to that pointer, and a pointer and a deleter initialize embedded pointer and deleter both.
UniquePtr<int> i1; assert(i1 == nullptr); UniquePtr<int> i2(new int(8)); assert(i2 != nullptr); UniquePtr<int, FreePolicy> i3(nullptr, FreePolicy());
Move construction and assignment
All remaining constructors and assignment operators accept only nullptr
or compatible, temporary UniquePtr
values. These values have well-defined ownership, in marked contrast to raw pointers.
class B { int i; public: B(int i) : i(i) {} virtual ~B() {} // virtual required so delete (B*)(pointer to D) calls ~D() }; class D : public B { public: D(int i) : B(i) {} }; UniquePtr<B> MakeB(int i) { typedef UniquePtr<B>::DeleterType BDeleter; // OK to convert UniquePtr<D, BDeleter> to UniquePtr<B>: // Note: For UniquePtr interconversion, both pointer and deleter // types must be compatible! Thus BDeleter here. return UniquePtr<D, BDeleter>(new D(i)); } UniquePtr<B> b1(MakeB(66)); // OK: temporary value moved into b1 UniquePtr<B> b2(b1); // ERROR: b1 not a temporary, would confuse // single ownership, forbidden UniquePtr<B> b3; b3 = b1; // ERROR: b1 not a temporary, would confuse // single ownership, forbidden b3 = MakeB(76); // OK: return value moved into b3 b3 = nullptr; // OK: can't confuse ownership of nullptr
What if you really do want to move a resource from one UniquePtr
to another? You can explicitly request a move using mozilla::Move()
from #include "mozilla/Move.h"
.
int* i = new int(37); UniquePtr<int> i1(i); UniquePtr<int> i2(Move(i1)); assert(i1 == nullptr); assert(i2.get() == i); i1 = Move(i2); assert(i1.get() == i); assert(i2 == nullptr);
Move
transforms the type of its argument into a temporary value type. Move
doesn’t have any effects of its own. Rather, it’s the job of users such as UniquePtr
to ascribe special semantics to operations accepting temporary values. (If no special semantics are provided, temporary values match only const
reference types as in C++98.)
Observing a UniquePtr
‘s value
The dereferencing operators (->
and *
) and conversion to bool
behave as expected for any smart pointer. The raw pointer value can be accessed using get()
if absolutely needed. (This should be uncommon, as the only pointer to the resource should live in the UniquePtr
.) UniquePtr
may also be compared against nullptr
(but not against raw pointers).
int* i = new int(8); UniquePtr<int> p(i); if (p) *p = 42; assert(p != nullptr); assert(p.get() == i); assert(*p == 42);
Changing a UniquePtr
‘s value
Three mutation methods beyond assignment are available. A UniquePtr
may be reset()
to a raw pointer or to nullptr
. The raw pointer may be extracted, and the UniquePtr
cleared, using release()
. Finally, UniquePtr
s may be swapped.
int* i = new int(42); int* i2; UniquePtr<int> i3, i4; { UniquePtr<int> integer(i); assert(i == integer.get()); i2 = integer.release(); assert(integer == nullptr); integer.reset(i2); assert(integer.get() == i2); integer.reset(new int(93)); // deletes i2 i3 = Move(integer); // better than release() i3.swap(i4); Swap(i3, i4); // mozilla::Swap, that is }
When a UniquePtr
loses ownership of its resource, the embedded deleter will dispose of the managed pointer, in accord with the single-ownership concept. release()
is the sole exception: it clears the UniquePtr
and returns the raw pointer previously in it, without calling the deleter. This is a somewhat dangerous idiom. (Mozilla’s smart pointers typically call this forget()
, and WebKit’s WTF calls this leak()
. UniquePtr
uses release()
only for consistency with unique_ptr
.) It’s generally much better to make the user take a UniquePtr
, then transfer ownership using Move()
.
Array fillips
UniquePtr<T>
and UniquePtr<T[]>
share the same interface, with a few substantial differences. UniquePtr<T[]>
defines an operator[]
to permit indexing. As mentioned earlier, UniquePtr<T[]>
by default will delete[]
its resource, rather than delete
it. As a corollary, UniquePtr<T[]>
requires an exact type match when constructed or mutated using a pointer. (It’s an error to delete[]
an array through a pointer to the wrong array element type, because delete[]
has to know the element size to destruct each element. Not accepting other pointer types thus eliminates this class of errors.)
struct B {}; struct D : B {}; UniquePtr<B[]> bs; // bs.reset(new D[17]()); // ERROR: requires B*, not D* bs.reset(new B[5]()); bs[1] = B();
And a mozilla::MakeUnique
helper function
Typing out new T
every time a UniquePtr
is created or initialized can get old. We’ve added a helper function, MakeUnique<T>
, that combines new
object (or array) creation with creation of a corresponding UniquePtr
. The nice thing about MakeUnique
is that it’s in some sense foolproof: if you only create new objects in UniquePtr
s, you can’t leak or double-delete unless you leak the UniquePtr
‘s owner, misuse a get()
, or drop the result of release()
on the floor. I recommend always using MakeUnique
instead of new
for single-ownership objects.
struct S { S(int i, double d) {} }; UniquePtr<S> s1 = MakeUnique<S>(17, 42.0); // new S(17, 42.0) UniquePtr<int> i1 = MakeUnique<int>(42); // new int(42) UniquePtr<int[]> i2 = MakeUnique<int[]>(17); // new int[17]() // Given familiarity with UniquePtr, these work particularly // well with C++11 auto: just recognize MakeUnique means new, // T means single object, and T[] means array. auto s2 = MakeUnique<S>(17, 42.0); // new S(17, 42.0) auto i3 = MakeUnique<int>(42); // new int(42) auto i4 = MakeUnique<int[]>(17); // new int[17]()
MakeUnique<T>(...args)
computes new T(...args)
. MakeUnique
of an array takes an array length and constructs the correspondingly-sized array.
In the long run we probably should expect everyone to recognize the MakeUnique
idiom so that we can use auto
here and cut down on redundant typing. In the short run, feel free to do whichever you prefer.
Update: Beware! Due to compiler limitations affecting gcc less than 4.6, passing literal nullptr
as an argument to a MakeUnique
call will fail to compile only on b2g-ics. Everywhere else will pass. You have been warned. The only alternative I can think of is to pass static_cast<T*>(nullptr)
instead, or assign to a local variable and pass that instead. Love that b2g compiler!
Conclusion
UniquePtr
was a free-time hacking project last Christmas week, that I mostly finished but ran out of steam on when work resumed. Only recently have I found time to finish it up and land it, yet we already have a couple hundred uses of it and MakeUnique
. Please add more uses, and make our existing new
code safer!
A final note: please use UniquePtr
instead of mozilla::Scoped
. UniquePtr
is more standard, better-tested, and better-documented (particularly on the vast expanses of the web, where most unique_ptr
documentation also suffices for UniquePtr
). Scoped
is now deprecated — don’t use it in new code!