3

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...

Quentin
  • 62,093
  • 7
  • 131
  • 191
Yaron Tausky
  • 803
  • 2
  • 8
  • 12
  • 1
    Who owns the `Foo` collection? That's probably where the deletion of Foos should go. – drRobertz Nov 07 '16 at 08:47
  • You should enforce this condition by code, this is so specific that I doubt you would found an "elegant" idiomatic way of doing it. Just design a sound system – Dredok Nov 07 '16 at 08:58
  • @Yaron Tausky: please have a look at my post, and mark it as the exact answer to your question! – Roland Nov 11 '16 at 13:31

2 Answers2

1

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();
}
Quentin
  • 62,093
  • 7
  • 131
  • 191
1

There is no idiomatic solution. Use shared_ptr and unique_ptr inside of the aggregate and weak_ptr as anchor to the aggregate. Here is my elaborated example of your problem:

#include <memory>
#include <iostream>
#include <vector>
using std::cout;
using std::endl;
using std::vector;
using std::shared_ptr;
using std::unique_ptr;
using std::weak_ptr;
using std::make_shared;
using std::make_unique;

struct Foo;
struct Bar
{   int b;
    shared_ptr<Foo> f;
    Bar(int b, shared_ptr<Foo> f): b(b), f(f)  { cout << "Constructor B with: " <<b  << std::endl; }
    ~Bar() { cout << "Destructor B with: " <<b << endl; }
};
struct Foo
{
    Foo()  { cout << "Constructor Foo" << endl; }
    ~Foo() { cout << "Destructor Foo"  << endl; }
    void clear()  { vb.clear(); }
    vector<unique_ptr<Bar>> vb;
};


int main(int argc, char* argv[])
{   weak_ptr<Foo> wf;  // Anchor to Aggregate
    {   // Construction of Aggregate
        vector<shared_ptr<Bar>> vb2;
        shared_ptr<Foo> f  =std::make_shared<Foo>();
        f->vb.emplace_back(make_unique<Bar>(1, f));
        f->vb.emplace_back(make_unique<Bar>(2, f));
        wf =f;
    }
    shared_ptr<Foo> f3 =wf.lock();
    if (f3)
    {
        if (argv[1][0] =='f')
        {
            cout <<"Destroy Foo" <<endl;
            f3->clear();
        }
        if (argv[1][0] =='b')
        {
            cout <<"Destroy Bar" <<endl;
            f3->vb[0].reset();
            f3->vb[1].reset();
        }
    }
}

Call the program with argument 'f' or 'b' and the output will be:

Constructor Foo
Constructor B with: 1
Constructor B with: 2
Destroy ???
Destructor B with: 1
Destructor B with: 2
Destructor Foo
Roland
  • 336
  • 2
  • 8