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_ptr
s 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 B
s 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.