2
votes

I was wondering how to use C++11 smart pointers correctly for graph representations.

Suppose, you have a graph structure which contains a vector of all its vertices. Furthermore, you have a structure/class of vertex. This vertex contains a vector of all its neighbors (adjacency list).

My question is: which types of pointers/smart pointers should I use, to represent this graph?

For binary trees, I read, for parent nodes you should use raw pointers. Because a node doesn't own its parent. The childs of a binary tree could be represented by std::unique_ptr because the node have the ownership of the childs.

But in a graph it is possible that multiple nodes have common neighbors. So, should I use a std::shared_ptr for this? Or should I use raw pointers, anyway?

1

1 Answers

8
votes

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.