2

I would like to implement this:

  • Object A owns an Object B (has a pointer to it)
    • When Object A is destroyed, Object B is destroyed too.
  • Object C has a std::vector of pointers to Object B-s.
    • When Object B is destroyed, remove its pointer from Object C's vector.

Is this possible with the combination of different smart pointers?

Tudvari
  • 2,715
  • 2
  • 14
  • 33
  • 2
    Would a [weak pointer](http://stackoverflow.com/questions/12030650/when-is-stdweak-ptr-useful) work for you? (Doesn't do exactly what you're asking for, but is the standard approach for this sort of problem.) – Allison Lock Dec 09 '16 at 12:08
  • 2
    Something like `struct A {std::unique_ptr b;};` and `struct C { std::vector> bs; };` ? – Jarod42 Dec 09 '16 at 12:08
  • 1
    That would mean `Object B` or its *smart pointer* would have to have some kind of reference to every *vector* that contains (smart?) pointers to it. That doesn't sound very practical tbh. – Galik Dec 09 '16 at 12:10
  • @Jarod42 That's what I was thinking about but I don't know that if I have an infinite loop in which I call a method on C's B-s, isn't it a bit ineffective to check weak_ptr.lock() every cycle instead of just signaling C when a B is destroyed? This weak pointer storing is very safe, but I don't know if it's effective enough for example for a game engine. – Tudvari Dec 09 '16 at 12:39
  • @Tudvari: A possible way is to mark `B` as is deleted, and let `C` deletes and removes object from its vector... or create an [observer](https://en.wikipedia.org/wiki/Observer_pattern). – Jarod42 Dec 09 '16 at 12:44
  • Doesn't that break the distinct roles? I mean isn't that bad, when the owner creates something, and then the observer has to delete it? – Tudvari Dec 09 '16 at 12:51
  • If you cannot ask to `c` to remove `b`, but only can ask `b` to kill himself, then `b` should know `c` in some way to allow it to remove it from its `vector`. – Jarod42 Dec 09 '16 at 12:54
  • @Jarod42: Your first comment would be better with `std::shared_ptr`. You can't create a `std::weak_ptr` from a `std::unique_ptr`. – Martin Bonner supports Monica Dec 09 '16 at 12:55
  • I think signaling C is slower than using expired() which is equals to use_count == 0. Which is just an int comparing. So I think I will use unique_ptr-weak_ptr combo, which only gives a small overhead, but it's very safe and saves a lot of time. – Tudvari Dec 09 '16 at 12:58
  • @MartinBonner: OP requires that destroying `a` destroy also `a`, so `shared_ptr` is not an option and its `b` should not be shared. – Jarod42 Dec 09 '16 at 12:59
  • But weak ptr cant be constructed from unique ptr. Can't I create a shared ptr, which has only 1 owner, and a non-owner (weak ptr)? – Tudvari Dec 09 '16 at 13:02
  • Why does A have a pointer to B, why doesn't it have an instance of B? Next, what should happen if, while iterating over the contents of the vector in C, code is run that would usually cause an A to be destroyed? – Yakk - Adam Nevraumont Dec 09 '16 at 13:58
  • Well. I don't know :D I don't know why didn't I realized this. I changed it. Now I think that the only option is signaling C. – Tudvari Dec 09 '16 at 14:29

2 Answers2

2

Let's think in terms of roles (and for now ignore threads):

Object A owns the lifetime of B

Object C is an observer of B's lifetime

You do not say whether there is a relationship between A and C so I will assume that A is aware of the involvement of C at the point of its constructor (let's use C as a configurable factory).

There are 2 places that B's lifetime events can create observations - the constructor / destructor of B (bad - tight coupling) or an intermediate factory (better - loose coupling).

So:

#include <memory>
#include <algorithm>

struct B {

};

struct C {
    std::shared_ptr<B> make_b() {
        auto p = std::shared_ptr<B>(new B(), [this](B *p) {
            this->remove_observer(p);
            delete p;
        });
        add_observer(p.get());
        return p;
    }

private:
    void add_observer(B *p) {
        observers_.push_back(p);
    }

    void remove_observer(B *p) {
        observers_.erase(std::remove(std::begin(observers_), std::end(observers_), p),
                         std::end(observers_));
    }

    std::vector<B *> observers_;
};

struct A {
    A(C &factory)
            : b_(factory.make_b()) {}


    std::shared_ptr<B> b_;

};


int main() {

    // note: factory must outlive a1 and a2
    C factory;

    A a1(factory);
    A a2(factory);
}

Note that while I have used a shared_ptr I could just have easily used a unique_ptr in this case. However I would then have coupled A with the deleter type in the pointer - so I'd either had to create my own type-erased deleter type or coupled A to C more tightly (which I wanted to avoid).

Richard Hodges
  • 68,278
  • 7
  • 90
  • 142
1
Object A owns an Object B (has a pointer to it)
    When Object A is destroyed, Object B is destroyed too.
Object C has a std::vector of pointers to Object B-s.
    When Object B is destroyed, remove its pointer from Object C's vector.

Object A can have its lifetime managed by shared_ptr.

It has full control over the lifetime of B:

struct A {
  std::unique_ptr<B> b;
};

or

struct A {
  B b;
};

We'll add an observe_B method:

struct A {
  std::unique_ptr<B> b;
  B* observe_B() { return b.get(); }
  B const* observe_B() const { return b.get(); }
};

which we'll make logically const. For the case where we have an actual B, we just do & instead of .get(). So we don't care how B is allocated (pointer or in the body of A) anymore.

Now we have a relatively complex lifetime requests. Judicious use of shared_ptr may be appropriate here. In fact, shared_from_this:

struct A:std::enable_shared_from_this<A> {
  std::unique_ptr<B> b;
  B* observe_B() { return b.get(); }
  B const* observe_B() const { return b.get(); }

  std::shared_ptr<B const> get_shared_B() const {
    return {shared_from_this(), observe_B()};
  }
  std::shared_ptr<B> get_shared_B() {
    return {shared_from_this(), observe_B()};
  }
};

Here we use the "aliasing constructor" of shared pointer to return a shared pointer to a non-shared object. It is intended for exactly this purpose. We use the shared lifetime semantics of A, but apply it to a B*.

In C we simply store a vector<weak_ptr>.

struct C {
  std::vector<std::weak_ptr<B>> m_Bs;
};

Now, when an A goes away, the weak_ptr to the "contained" B loses its last strong reference. When you .lock() it, it now fails.

struct C {
  std::vector<std::weak_ptr<B>> m_Bs;
  void tidy_Bs() {
    auto it = std::remove_if( begin(m_Bs), end(m_Bs), [](auto&& x)->bool{return !x.lock();});
    m_Bs.erase(it, end(m_Bs));
  }
};

tidy_Bs removes all of the "dangling" weak_ptrs to B in m_Bs.

To iterate, I'd typically do this:

struct C {
  std::vector<std::weak_ptr<B>> m_Bs;
  void tidy_Bs() {
    auto it = std::remove_if( begin(m_Bs), end(m_Bs), [](auto&& x)->bool{return !x.lock();});
    m_Bs.erase(it, end(m_Bs));
  }

  template<class F>
  void foreach_B(F&& f) {
    tidy_Bs();
    auto tmp = m_Bs;
    for (auto ptr:m_Bs)
      if (auto locked = ptr.lock())
        f(*locked);
  }
};

which passes the f a B& for each of the still existing Bs in the m_Bs vector. While it is in there, it cleans up the dead ones.

I copy the vector because while iterating, someone could go and change the contents of m_Bs, and to be robust I cannot be iterating over m_Bs while that is happening.

This entire technique can be done without A being managed by shared_ptr; but then B has to be managed by shared_ptr.

Note that an operation that would "normally" cause A to be destroyed may not actually do it if C currently has a .lock() on the B contained within A. Practically there is no way to avoid that, other than making C crash.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524