• A raw pointer. With this approach, if A
is destroyed, but C
continues to point to B
, B
will contain a pointer to A
that will dangle. B
won't be able to detect that, so B
may inadvertently dereference the dangling pointer. That would yield undefined behavior.
• A std::shared_ptr
. In this design, A
and B
contain std::shared_ptr
s to each other. The resulting std::shared_ptr
cycle ( A
points to B
and B
points to A
) will prevent both A
and B from being destroyed. Even if A
and B
are unreachable from other program data structures (e.g., because C
no longer points to B
), each will have a reference count of one. If that happens, A
and B
will have been leaked, for all practical purposes: it will be impossible for the program to access them, yet their resources will never be reclaimed.
• A std::weak_ptr
. This avoids both problems above. If A
is destroyed, B
's pointer back to it will dangle, but B
will be able to detect that. Furthermore, though A
and B
will point to one another, B
's pointer won't affect A
's reference count, hence can't keep A
from being destroyed when std::shared_ptr
s no longer point to it.
Using std::weak_ptr
is clearly the best of these choices. However, it's worth noting that the need to employ std::weak_ptr
s to break prospective cycles of std::shared_ptr
s is not terribly common. In strictly hierarchal data structures such as trees, child nodes are typically owned only by their parents. When a parent node is destroyed, its child nodes should be destroyed, too. Links from parents to children are thus generally best represented by std::unique_ptr
s. Back-links from children to parents can be safely implemented as raw pointers, because a child node should never have a lifetime longer than its parent. There's thus no risk of a child node dereferencing a dangling parent pointer.
Of course, not all pointer-based data structures are strictly hierarchical, and when that's the case, as well as in situations such as caching and the implementation of lists of observers, it's nice to know that std::weak_ptr
stands at the ready.
From an efficiency perspective, the std::weak_ptr
story is essentially the same as that for std::shared_ptr
. std::weak_ptr
objects are the same size as std::shared_ptr
objects, they make use of the same control blocks as std::shared_ptr
s (see Item 19), and operations such as construction, destruction, and assignment involve atomic reference count manipulations. That probably surprises you, because I wrote at the beginning of this Item that std::weak_ptr
s don't participate in reference counting. Except that's not quite what I wrote. What I wrote was that std::weak_ptr
s don't participate in the shared ownership of objects and hence don't affect the pointed-to object's reference count . There's actually a second reference count in the control block, and it's this second reference count that std::weak_ptr
s manipulate. For details, continue on to Item 21.
Things to Remember
• Use std::weak_ptr
for std::shared_ptr
-like pointers that can dangle.
• Potential use cases for std::weak_ptr
include caching, observer lists, and the prevention of std::shared_ptr
cycles.
Item 21: Prefer std::make_unique
and std::make_shared
to direct use of new
.
Let's begin by leveling the playing field for std::make_unique
and std::make_shared
. std::make_shared
is part of C++11, but, sadly, std::make_unique
isn't. It joined the Standard Library as of C++14. If you're using C++11, never fear, because a basic version of std::make_unique
is easy to write yourself. Here, look:
template
std::unique_ptr make_unique(Ts&&... params) {
return std::unique_ptr(new T(std::forward(params)...));
}
As you can see, make_unique
just perfect-forwards its parameters to the constructor of the object being created, constructs a std::unique_ptr
from the raw pointer new
produces, and returns the std::unique_ptr
so created. This form of the function doesn't support arrays or custom deleters (see Item 18), but it demonstrates that with only a little effort, you can create make_unique
if you need to. [9] To create a full-featured make_unique with the smallest effort possible, search for the standardization document that gave rise to it, then copy the implementation you'll find there. The document you want is N3656 by Stephan T. Lavavej, dated 2013-04-18.
Just remember not to put your version in namespace std
, because you won't want it to clash with a vendor-provided version when you upgrade to a C++14 Standard Library implementation.
std::make_unique
and std::make_shared
are two of the three make
functions : functions that take an arbitrary set of arguments, perfect-forward them to the constructor for a dynamically allocated object, and return a smart pointer to that object. The third make
function is std::allocate_shared
. It acts just like std::make_shared
, except its first argument is an allocator object to be used for the dynamic memory allocation.
Even the most trivial comparison of smart pointer creation using and not using a make
function reveals the first reason why using such functions is preferable. Consider:
auto upw1(std::make_unique< Widget>()); // with make func
std::unique_ptr< Widget> upw2(new Widget); // without make func
auto spw1(std::make_shared< Widget>()); // with make func
std::shared_ptr< Widget> spw2(new Widget); // without make func
I've highlighted the essential difference: the versions using new
repeat the type being created, but the make
functions don't. Repeating types runs afoul of a key tenet of software engineering: code duplication should be avoided. Duplication in source code increases compilation times, can lead to bloated object code, and generally renders a code base more difficult to work with. It often evolves into inconsistent code, and inconsistency in a code base often leads to bugs. Besides, typing something twice takes more effort than typing it once, and who's not a fan of reducing their typing burden?
The second reason to prefer make
functions has to do with exception safety. Suppose we have a function to process a Widget
in accord with some priority:
void processWidget(std::shared_ptr spw, int priority);
Passing the std::shared_ptr
by value may look suspicious, but Item 41explains that if processWidget
always makes a copy of the std::shared_ptr
(e.g., by storing it in a data structure tracking Widget
s that have been processed), this can be a reasonable design choice.
Читать дальше