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.