5

I have not yet found the following way of breaking cyclic references explained on any major C++ forum/blog, like on GotW, so I wanted to ask whether the technique is known, and what are its pro and cons?

class Node : public std::enable_shared_from_this<Node> {
public:
   std::shared_ptr<Node> getParent() {
      return parent.lock();    
   }

   // the getter functions ensure that "parent" always stays alive!
   std::shared_ptr<Node> getLeft() {
       return std::shared_ptr<Node>(shared_from_this(), left.get());
   }

   std::shared_ptr<Node> getRight() {
       return std::shared_ptr<Node>(shared_from_this(), right.get());
   }

   // add children.. never let them out except by the getter functions!
public:
   std::shared_ptr<Node> getOrCreateLeft() {
       if(auto p = getLeft())
          return p;
       left = std::make_shared<Node>();
       left->parent = shared_from_this();
       return getLeft();
   }

   std::shared_ptr<Node> getOrCreateRight() {
       if(auto p = getRight())
          return p;
       right = std::make_shared<Node>();
       right->parent = shared_from_this();
       return getRight();
   }

private:
   std::weak_ptr<Node> parent;
   std::shared_ptr<Node> left;
   std::shared_ptr<Node> right;
};

From the outside, the user of Node will not notice the trick with using the aliasing constructor in getLeft and getRight, but still the user can be sure that getParent always returns a non-empty shared pointer, because all pointers returned by p->get{Left,Right} keep the object *p alive for the lifetime of the returned child pointer.

Am I overlooking something here, or is this an obvious way to break cyclic references that has been documented already?

int main() {
   auto n = std::make_shared<Node>();
   auto c = n->getOrCreateLeft();
   // c->getParent will always return non-null even if n is reset()!
}
curiousguy
  • 8,038
  • 2
  • 40
  • 58
Johannes Schaub - litb
  • 496,577
  • 130
  • 894
  • 1,212
  • Does this mean that ultimately *all children* will share the same reference count as the root? That is, if I create `left_A` with the aliasing constructor, then it's reference count is the same as `parent`. Then if I create a new `left_B` off `left_A`, and use `left_A->shared_from_this` - is the reference count still `parent`, as in the indirection continues up the tree? In that case all nodes share the same reference count, and you can never remove a node and recycle its resources until the entire tree is removed? – Steve Lorimer May 31 '16 at 17:18
  • @SteveLorimer they do not share the root's reference count unless they have been given to the outside world by one of the `get` functions of a node (which in turn would have been given by another node .. and so on, starting from the root). So unless someone has a reference to a child node, (which perhaps only is the case temporarily when walking the treeor during an asynchronous operation), the reference count is not shared, and resources can be freed. – Johannes Schaub - litb May 31 '16 at 17:22
  • I proposed a [solution](https://stackoverflow.com/a/64056443/196429) that might be relevant in specific circumstances. – sircolinton Sep 25 '20 at 01:41

1 Answers1

3

The shared_ptr<Node> returned by your getParent owns the parent, not the parent's parent.

Thus, calling getParent again on that shared_ptr can return an empty (and null) shared_ptr. For example:

int main() {
   auto gp = std::make_shared<Node>();
   auto p = gp->getOrCreateLeft();
   auto c = p->getOrCreateLeft();
   gp.reset();
   p.reset(); // grandparent is dead at this point
   assert(c->getParent());
   assert(!c->getParent()->getParent());
}

(The inherited shared_from_this also passes out shared_ptrs that owns the node rather than its parent, but I suppose you can make it harder to mess up by a private using declaration and ban it by contract.)

T.C.
  • 133,968
  • 17
  • 288
  • 421