In praise of make_unique
make_unique
Lately I’ve been working on a codebase that does a lot of this:
class Widget {
using ptr = std::shared_ptr<Widget>;
// ...
};
Widget::ptr p(new Widget(stuff));
// ...
p.reset(new Widget(otherstuff));
// ...
p.reset();
This style has advantages and disadvantages — but mostly, disadvantages.
The (lone?) advantage is that there is only a single mention of std::
anywhere in
this code, which means that it’s very easy to swap out std::shared_ptr
for boost::shared_ptr
or any other smart pointer. (Actually, in my codebase,
the most likely candidate would be our hand-written smart pointer that does
intrusive ref-counting and uses RCU for reclamation.)
The other day, someone on Slack mentioned a disadvantage of this style: they’d been using this style and accidentally wrote the last line above as
p.release(); // oops, meant "p.reset"
This leaks the shared_ptr
instead of freeing it. And compilers don’t diagnose
this mistake (because p.release();
is arguably a reasonable thing to write,
if you precede it with q.take_ownership_of(p.get())
).
I claim that the problem of “misspelling the name of an obscure mutator method”
goes away entirely if you make a habit of not using obscure mutator methods.
So, I refactor the Widget
snippet this way:
class Widget {
// ...
};
auto p = std::make_shared<Widget>(stuff);
// ...
p = std::make_shared<Widget>(otherstuff);
// ...
p = nullptr;
This style loses the ability to switch to boost::shared_ptr
with a one-line patch,
but it gains several other advantages:
-
No raw
new
anddelete
. I try to make it so thatgit grep -l 'new '
returns no results, or at least returns a small O(1) number of results (for example, a single use of placementnew
syntax deep in the guts of some utility type). If your code is littered with.reset(new ...)
, then you’ll never be able to find that one memory leak caused by an unmanagednew
. -
Fewer allocations, because of how
make_shared
works. -
More readable by novice programmers; you might have to look up what
reset
means, butoperator=
is immediately intuitive (to curly-brace programmers, anyway). -
No chance of typo’ing
reset
asrelease
.
And of course the same thing goes for std::unique_ptr
and
std::make_unique
,
which is why it’s so nice that we finally got std::make_unique
in C++14.
The versatile unique_ptr
One little-known but very useful fact about std::unique_ptr
and std::shared_ptr
is that the former implicitly converts to the latter!
template<class Y, class Deleter>
shared_ptr(std::unique_ptr<Y, Deleter>&& r);
So if we have a codebase that mixes shared_ptr
and unique_ptr
(perhaps we’re
in the process of converting the former to the latter wherever possible), it is
convenient that
myptr_ = std::make_unique<T>();
always compiles, regardless of whether myptr_
is a std::shared_ptr<T>
or a std::unique_ptr<T>
.
Should make_widget
return unique_ptr
?
This means that if you have a factory function that returns a new object, you
may be better off making it return unique_ptr
than shared_ptr
, even if you
plan usually to use the result as a shared_ptr
!
Here are some common usage patterns for such a factory function:
std::shared_ptr<Widget> make_widget() {
return std::make_shared<WidgetImpl>(stuff);
}
class WidgetTestHarness {
std::shared_ptr<Widget> widget_;
WidgetTestHarness() {
widget_ = make_widget();
}
};
void view_widget(Widget& w);
void keep_widget(std::shared_ptr<Widget> sptr);
void manipulator() {
auto w = make_widget();
keep_widget(w);
view_widget(*w);
}
Notice (well, suppose) that our WidgetTestHarness
never needs to copy its
shared_ptr<Widget>
. Really it just wants a member Widget widget_;
, but it
can’t do that because the only way to create a Widget
object is via make_widget
.
(And notice why that is: make_widget()
secretly creates a WidgetImpl
object,
not a base Widget
!) So it would settle for holding a unique_ptr<Widget>
,
if it could.
Philosophically, it makes sense that make_widget()
should return a unique_ptr
.
I mean, it’s creating a brand-new Widget
. There’s no way that anyone else in
the program could already hold a reference to this Widget
. So the pointer returned
from make_widget
is, literally, a unique pointer. We are certainly tempted to
represent that unique pointer using unique_ptr
.
Let’s see if we can make that happen.
std::unique_ptr<Widget> make_widget() {
return std::make_unique<WidgetImpl>(stuff); // Danger, Will Robinson!
}
class WidgetTestHarness {
std::unique_ptr<Widget> widget_;
WidgetTestHarness() {
widget_ = make_widget();
}
};
void view_widget(Widget& w);
void keep_widget(std::shared_ptr<Widget> sptr);
void manipulator() {
std::shared_ptr<Widget> w = make_widget(); // Slightly awkward.
keep_widget(w);
view_widget(*w);
}
This rewrite gets us the behavior we want — unique_ptr
in WidgetTestHarness
— with
relatively little downside. I’ve thought of two downsides, annotated above.
Downside number one: When we convert unique_ptr<WidgetImpl>
to unique_ptr<Widget>
,
we change the deleter as well, from default_delete<WidgetImpl>
(which calls ~WidgetImpl
)
to default_delete<Widget>
(which calls ~Widget
). If ~Widget
is not virtual
, then
we have introduced a bug. So we’d better make dang sure that our Widget
has a virtual
destructor, if we’re going to be using this pattern with make_widget
.
I could swear that sometimes Clang is smart enough to diagnose the accidental call to
Widget
’s non-virtual destructor, but not in this case.
(GCC also does not diagnose it, but I wouldn’t expect them to.)
Downside number two: If we keep the auto w
in manipulator()
, then the call to
keep_widget(w)
won’t compile. You can implicitly convert an rvalue unique_ptr
to
shared_ptr
(the shared_ptr
just steals ownership from the unique_ptr
),
but you can’t convert an lvalue unique_ptr
to shared_ptr
(because if we stole the
ownership from w
, then the following line’s view_widget(*w)
would blow up with a null
pointer dereference).
Therefore, we had to replace auto w
with std::shared_ptr<Widget> w
, which
might be too much typing for some people (and makes the patch even bigger if we ever want to
switch to boost::shared_ptr
).
Notice that in C++17 we can write simply
std::shared_ptr w = make_widget();
which is still a lot of typing, and incidentally I’m not a fan of gratuitous CTAD,
and incidentally this line won’t work on libc++ yet because they have not implemented
deduction guides for shared_ptr
yet.
But libstdc++ has implemented them; and presumably libc++ will follow suit relatively soon.
(C++17 deduction guides for the STL containers are
landing in libc++ as we speak!)