You must first design a node (or edge) ownership strategy.
For example, in the binary tree example you site, a parent owns its child nodes, but not its parent node. This design ensures that every node in the tree is owned by exactly one other node, except for the root node, which you can make a special case out of. Since in this example, each node has exactly one owner (its parent), unique_ptr
can be used to model this relationship. Also in this example, the link from the child to the parent is a non-owning link, and so can not be modeled with an owning smart pointer.
In the binary tree example, your owning graph is acyclic, directed, and every node is pointed to only once (by owning pointers).
In a more complicated example, the graph might be acyclic, directed, but nodes can be pointed to more than once. Such a graph could use shared_ptr
to model the pointed-to links since such links share ownership of the pointee.
However one must be careful. As soon as your graph becomes cyclic, then shared_ptr
can no longer be exclusively used. Any time you set up a cycle of ownership:
A owns B which owns C which owns A
then you set up a memory leak. Such cycles can be created with either shared_ptr
or unique_ptr
. But in practice, cycles tend to happen more often with the use of shared_ptr
, probably just because the ownership diagram is inherently more complex than that of unique_ptr
.
shared_ptr
contains a helper class to break cyclic ownership patterns: weak_ptr
. This can be used to set up something like:
A owns B which owns C which has a (non-owning) weak_ptr to A
If the graph is undirected, and you model nodes A and B with:
A points to B and B points to A
then you immediately set up a cycle, and so both of these pointers can not be owning pointers. In such a situation, you will have to design what owns what. Perhaps a completely separate piece of code could own all of the nodes, and all of the edges can be represented with non-owning pointers. Perhaps the graph can be partitioned into a set of acyclic directed edges (representing owning pointers), and all other edges (non-owning pointers). Indeed, this is exactly what we did with the binary tree -- owning child pointers and non-owning parent pointers.
Whatever your design, once it is done, your design will lead you to whether shared_ptr
and/or unique_ptr
are appropriate tools to implement your design. If something is always going to be uniquely owned, unique_ptr
is a viable option. If something needs to be owned by several other entities, shared_ptr
might be the correct tool (and definitely not unique_ptr
). If the shared_ptr
ownership graph contains cycles, they can be broken by replacing some of those links with weak_ptr
. If you fail to detect and break cycles in your ownership graph, the cycles will result in memory leaks.