[solved] Why is my ReferenceCountedObject asserting?

This should be a simple problem for a sharper mind to solve, but I’m completely stumped.

I have a class that creates itself recursively into a tree, where each node has one pointer to its parent and one to its child. I thought that this would be a good place to use ReferenceCountedObjectPtr, since each node is pointed to by multiple others. But I get an assertion when closing the application. Can anyone explain why?

(I’ve simplified the example so that the tree doesn’t branch).

class RefCountTest : public ReferenceCountedObject
{
public:
    RefCountTest(int i, RefCountTest* const parent) : m_parent(parent)
    { 
        if (i > 0)
            m_child = new RefCountTest(--i, this); DBG(i);
    }
private:
    ReferenceCountedObjectPtr <RefCountTest> m_child, m_parent;
};

 RefCountTest test { 6, nullptr };

I know the theory behind shared pointers, but I’m only just beginning to use them, so it’s possible that I’m misunderstanding something fundamental about them. If my approach seems drastically wrong then please share.

Since each parent keeps a reference to the child, and the child keeps a reference to the parent, they will never go out of scope. So you get a leak at shutdown.

You need to break that cycle by using some kind of non-owning reference, e.g. WeakReference.
The std::shared_ptr has the std::weak_ptr specifically for that purpose…

Time for me to learn about weak references then!

Is there a JUCE class for this, or should I look towards the std objects you mentioned?

There is: WeakReference

So I would suggest to keep references to the children as ReferenceCountedObject::Ptr and a WeakReference to the parent. But technically either way is fine.

lass RefCountTest : public ReferenceCountedObject
{
public:
    RefCountTest (int i, RefCountTest* const parent) : m_parent(parent)
    { 
        if (i > 0)
            m_child = new RefCountTest(--i, this); DBG(i);
    }
    ~RefCountTest()
    {
        masterReference.clear();
    }
private:
    ReferenceCountedObjectPtr <RefCountTest> m_child;
    WeakReference<RefCountTest> m_parent;
    JUCE_DECLARE_WEAK_REFERENCEABLE (RefCountTest)
};

The WeakReference is automatically set to nullptr, when the referenced object is deleted.

2 Likes

Thanks for the fast response and clear explanations. I must have read about the difference between shared and weak pointers a dozen times, but I never really understood it before I stumbled into this problem.

The JUCE_DECLARE_WEAK_REFERENCEABLE macro is really useful to know about.

Great that I could help.

The smart pointers are about defining ownership and lifetime. The unique_ptr owns an object, in the case of shared_ptr (and ReferenceCountedObject), the ownership is floating between all references, that are kept around. Therefore it is sometimes hard to determine, when (and if) an object is destroyed.

They are very handy in some situations, but often they are also problematic. The scenario you showed is a great example. I saw users replacing all pointers with smart_ptr, which is a bad idea, better to find a hierarchy, what belongs where, and define the ownership there.

Thanks for the feedback. In your opinion, does my case call for shared pointers, or would unique or even simple pointers do? Just to flesh it out a little more,

  • Every node points to its parent (one) and its children (multiple)
  • Nodes can be moved from one point in the tree to another
  • If a node is deleted, all of its children are also deleted.

Originally I was using OwnedArray for the children and a raw pointer for the parent. I thought I’d try shared pointers because they seemed safer, but now I’m wondering whether it might actually be over complicating things.

I think that is the best approach, with the addition to use a std::unique_ptr for the root.

Like I said, the ReferenceCountedObject is useful, if you have an object that is held by many, and it is not predictable, when it is no longer needed.
Another use case is, when it is shared between threads, so you can make sure, it doesn’t get deleted by holding on to a reference (but careful with realtime threads, the destruction will happen at any place).
But they are very cumbersome to model interdependencies, when anything can reference anything. Then it is easy to fall in that trap of cyclic references.