2

I've read here that, in a situation where some parent object uniquely owns several children, and each child needs to be able to access its parent, a weak pointer should be used as the back-pointer instead of a shared pointer so as to avoid a dependency cycle.

I have two questions:

  1. What are the advantages of giving a weak pointer to the parent as opposed to a simple reference?
  2. In the code below, how should I pass each child a weak pointer to its parent? I know you can inherit from std::enable_shared_from_this to get a shared pointer from this. Is there an equivalent for weak pointers? Or something else?
#include <memory>
#include <vector>

class Parent;

class Child {
 public:
  void set_parent(std::weak_ptr<Parent> parent) {
    parent_ = parent;
  }
  std::weak_ptr<Parent> parent_;
};

class Parent {
  void add_child(std::unique_ptr<Child> child) {
    // child->set_parent(???);
    children_.push_back(std::move(child));
  }
  std::vector<std::unique_ptr<Child>> children_;
};
  • 1
    As an aside: I took apart a design a coworker did a few years ago and rewrote it from shared_ptr to raw pointers. The items were always owned by the tree and were privately held. Class invariants guaranteed correct deletion. Without all the shared and weak pointers it was more efficient AND easier to read. – Zan Lynx Mar 10 '21 at 18:29
  • @ZanLynx I've only recently started learning C++, but everybody seems to suggest to use smart pointers instead of raw pointers? Also, just out of interest, what do you mean when you say "owned by the tree"? –  Mar 10 '21 at 18:36
  • 1
    Also note that if you *do* use shared_ptr for tree elements and hand out shared_ptr's to callers you can end up destroying the tree and holding subtree's which are alive because some other code is holding onto them. That can lead to Java style errors where code holds megabytes of tree elements because it wants to use a 30 byte string that it should have copied. – Zan Lynx Mar 10 '21 at 18:37
  • 2
    Smart pointers are usually better but cyclic data structures are not one of the cases. And look at the standard `std::set` which is implemented as a binary tree. The C++ library implementations use pointers, not smart pointers. – Zan Lynx Mar 10 '21 at 18:40
  • 2
    Raw pointers in c++ mean: The recipient of the pointer passed as a raw pointer does not _own_ it therefore should _not_ delete it. They are used in cases where the recipient will not store them away, or if it does, will guarantee the place where it is stored will _not live longer_ than the _caller_. If you can make these guarantees then use the raw pointer: That's what they mean (now, since C++11). In this case if the children live _no longer than the tree itself_ (that is, their parents) then you're good. Just make sure, on deletion of the tree, to delete from the bottom up. – davidbak Mar 10 '21 at 18:41
  • Thanks a lot @ZanLynx this is very helpful. –  Mar 10 '21 at 18:41

2 Answers2

4

weak_ptrs come from shared_ptrs. If your parent nodes "uniquely own" the children, then the children cannot have weak_ptrs to their parents, as this would require that a shared_ptr to them exist somewhere.

Note that the example you cite does not state anything about "unique ownership" of the relationship between the items.

Furthermore, so long as the parents never relinquish ownership of the children to code outside of the system, there's no reason to not just have a regular pointer to the parent (it needs to be a pointer because a reference cannot be changed). The pointer will always be valid (from outside of the system), since a parent must exist in order for the child to exist. That's what unique ownership means, after all.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • Ah I see. Thanks a lot for your answer. Yes, my mistake about the link I cited not specifically referring to unique ownership. –  Mar 10 '21 at 18:40
  • 1
    The child *absolutely* can have a reference to their parent, but it would have to be passed in as an argument to the child's constructor, and have the reference as a member variable, initialized again, by the constructor. That's how I usually do it in this type of thing, as then there's no "is it null?" ambiguity, and it's also clear the child doesn't own the parent in any way (regular pointers are ambiguous IMO). And keep in mind all the stuff kanoisa said below too – Kevin Anderson Mar 10 '21 at 20:15
  • Thank you Kevin. Although I want my `child` objects to be created outside of the parents, and *then* later added via `std::move`, so am forced to use a pointer. –  Mar 10 '21 at 20:17
4

You have said that a parent always uniquely owns a child but you have not specified a few things:

  1. Is it valid to copy a child object?
  2. Is it valid to move a child object?

If you answered yes to either of those then you must consider what happens if a child is copied or moved between parents as your child class wont by default prevent it. So my advice would be to either explicitly delete copy and move constructors and assignment operators OR if you want that ability then the responsibility is on YOU to make sure that if and when a child is copied and or moved that its parent pointer points to the correct parent and to prevent it pointing to an invalid / deleted parent.

If you go down the route of explicitly disabling copy and move semantics then in that circumstance i can't think of anything off the top of my head that would prevent you from using a reference to the parent instead.

Finally if you did actually want weak pointers in the children pointing to their parents then you could consider having your parent class extend from : enable_shared_from_this. I think the typical usecase for this is when you don't know if you will outlive another object but in the case where it is still a valid object then you would like to ensure it stays alive while you perform some operations on it.

Please note if you take that approach the parent object MUST be stored inside a shared_ptr. See shared_from_this. If not then you are in undefined behaviour territory and lets just all agree that is bad.

Edit: i forgot to mention if you can use C++17 you can directly get the weak_ptr from the parent to pass to the child: weak_from_this

kanoisa
  • 390
  • 1
  • 5
  • Thanks kanoisa, this seems like excellent advice. Never knew about `weak_from_this`. –  Mar 10 '21 at 20:19