Search code examples
c++smart-pointersraiiobject-lifetime

How do I elegantly use smart pointers to model complex lifetimes in C++?


Suppose I have a collection of Foo objects, and each Foo owns one or more Bar objects. A specific Foo or Bar can be deleted by a user of my interface; when a Foo is deleted, all the Bars it owns are deleted too. So far, giving each Foo a collection of unique_ptr<Bar> is all I need to automatically manage this model. However, I have an additional requirement: once a Foo is left without any Bar, it should also be deleted.

Of course, I could just write code that explicitly takes care of that, but I wonder if there's a more idiomatic way to achieve the same thing. That sounds an awful lot like a shared_ptr, but not quite...


Solution

  • Since deleting all Bars from a Foo should delete the Foo, it may indeed sound like you need a handful of shared_ptrs from Bars to their Foo.

    However, that model would place the lifetime of the Foo in the hands of its Bars: you wouldn't be able to directly delete the Foo, instead you'd have to hunt down all of its Bars and delete these.

    Foo would keep Bar*s instead of unique_ptr<Bar>s, since it cannot die before its Bars. But then you have to give ownership of the Bars to someone...

    You might end up with yet another object that holds the collection of unique_ptr<Bar>s corresponding to each of the Foos, but then you have to keep all of this in sync as Bars come and go. That's the same kind of bookkeeping you're trying to avoid, but a lot bigger, more complicated, and more brittle as a result, with memory leaks and rogue pointers as failure cases.


    So instead of all of this, I suggest you stick with your first unique_ptr-powered idea. A first go at an implementation might look like this:

    struct Foo {
    private:
        friend void remove(std::unique_ptr<Foo> &foo, Bar const *bar);
    
        // Removes the bar from this Foo.
        // Returns true iff the Foo is now empty and should be deleted.
        bool remove(Bar const *bar) {
            auto i = std::find_if(begin(_bars), end(_bars), [&](auto const &p) {
                return p.get() == bar;
            });
    
            assert(i != end(_bars));
            _bars.erase(i);
    
            return _bars.empty();
        }
    
        std::vector<std::unique_ptr<Bar>> _bars;
    };
    
    // Removes the bar from the Foo.
    // Deletes the Foo if it becomes empty.
    void remove(std::unique_ptr<Foo> &foo, Bar const *bar) {
        if(foo->remove(bar))
            foo.reset();
    }