Jeff Walden: mfbt now has UniquePtr and MakeUnique for managing singly-owned resources |
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_ptrauto_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.
auto_ptr
Implicit-move semantics are safe but very unclear. And because these operations mutate their input, they cant 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 Ill use temporary value for readability.) If the inputs a temporary value, we can move the resource out of it without disrupting anyone elses view of it: as a temporary itll die before anyone could observe it. (The rvalue reference concept is incredibly subtle. Read that article series a dozen times, and maybe youll understand half of it. Ive spent multiple full days digesting it and still wont claim full understanding.)
mozilla::UniquePtr
Ive implemented mozilla::UniquePtr
in #include "mozilla/UniquePtr.h"
to fit the bill. Its 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 parametersUsing 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[]
.)
UniquePtri1(new int(8)); UniquePtr 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, its a really good idea for it to be empty (per mozilla::IsEmpty
) so that UniquePtr
is as space-efficient as a raw pointer.
struct FreePolicy { void operator()(void* ptr) { free(ptr); } }; { void* m = malloc(4096); UniquePtr mem(m); int* i = static_cast(malloc(sizeof(int))); UniquePtr integer(i); // integer.getDeleter()(i) is called // mem.getDeleter()(m) is called }
UniquePtr
construction and assignmentAs youd 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.
UniquePtri1; assert(i1 == nullptr); UniquePtr i2(new int(8)); assert(i2 != nullptr); UniquePtr i3(nullptr, FreePolicy());
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 MakeB(int i) { typedef UniquePtr::DeleterType BDeleter; // OK to convert UniquePtr to UniquePtr: // Note: For UniquePtr interconversion, both pointer and deleter // types must be compatible! Thus BDeleter here. return UniquePtr(new D(i)); } UniquePtr b1(MakeB(66)); // OK: temporary value moved into b1 UniquePtr b2(b1); // ERROR: b1 not a temporary, would confuse // single ownership, forbidden UniquePtr 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); UniquePtri1(i); UniquePtr 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
doesnt have any effects of its own. Rather, its 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.)
UniquePtr
s valueThe 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); UniquePtrp(i); if (p) *p = 42; assert(p != nullptr); assert(p.get() == i); assert(*p == 42);
UniquePtr
s valueThree 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; UniquePtri3, i4; { UniquePtr 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. (Mozillas smart pointers typically call this forget()
, and WebKits WTF calls this leak()
. UniquePtr
uses release()
only for consistency with unique_ptr
.) Its generally much better to make the user take a UniquePtr
, then transfer ownership using Move()
.
UniquePtr
and UniquePtr
share the same interface, with a few substantial differences. UniquePtr
defines an operator[]
to permit indexing. As mentioned earlier, UniquePtr
by default will delete[]
its resource, rather than delete
it. As a corollary, UniquePtr
requires an exact type match when constructed or mutated using a pointer. (Its 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 bs; // bs.reset(new D[17]()); // ERROR: requires B*, not D* bs.reset(new B[5]()); bs[1] = B();
mozilla::MakeUnique
helper functionTyping out new T
every time a UniquePtr
is created or initialized can get old. Weve added a helper function, MakeUnique
, that combines new
object (or array) creation with creation of a corresponding UniquePtr
. The nice thing about MakeUnique
is that its in some sense foolproof: if you only create new objects in UniquePtr
s, you cant 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) {} }; UniquePtrs1 = MakeUnique(17, 42.0); // new S(17, 42.0) UniquePtri1 = MakeUnique (42); // new int(42) UniquePtr i2 = MakeUnique(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 (17, 42.0); // new S(17, 42.0) auto i3 = MakeUnique(42); // new int(42) auto i4 = MakeUnique(17); // new int[17]()
MakeUnique
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.
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 dont use it in new code!