A std::shared_ptr
destroys the object it's managing when the reference count hits 0. However, I'm looking for a type of smart pointer where the object gets destroyed when the reference count drops below 2. Is there a smart pointer that will behave like this (or can I make a smart pointer behave like this, in a safe way)?
Use case scenario: I'm modelling a connection. The connection is owned (as in "ownership by a smart pointer") by the two end points it connects. As soon as one of the end points gets destroyed, the connection should be destroyed as well.
I know I could achieve this with classic delete
statements in the appropriate destructors (since my requirement of "below 2" is super easy in this case). But I think this is a valid use case for a type of smart pointer, and I'm curious to see if I could do this using a modern way.
Rc<T> , a reference counting type that enables multiple ownership.
Shared pointers are smart pointers that keep a count of how many instances of the pointer exist, and only clean up the memory when the count reaches zero. In general, only use shared pointers (but be sure to use the correct kind--there is a different one for arrays).
The shared_ptr type is a smart pointer in the C++ standard library that is designed for scenarios in which more than one owner might have to manage the lifetime of the object in memory.
Probably the easiest solution is for each side to have a shared_ptr
to the object, a weak_ptr
to the object, and a regular pointer to the other side's shared_ptr
.
To access the object, you lock the weak_ptr
. If it fails, the object is gone.
To destroy yourself, you lock the weak_ptr
, reset the other side's shared_ptr
through your regular pointer to it, reset your own shared_ptr
, then get rid of the result of the lock on the weak_ptr
.
Alternatively, you can just use a counter and a regular pointer. If the counter is 1, you know the other side is gone, so you can just destroy the object.
Thanks for including what effect you're trying to achieve.
You don't want or need any special logic in the smart pointer. Both ends need an ordinary strong reference to the shared object. That cleanly expresses that it lives as long as either side knows about it.
What you do want instead is an event notification where either side can notify the other when it leaves. Then the surviving side can cleanly do the proper cleanup, including setting its (now last) shared_ptr to null, but also doing anything else that is logically required.
As you have two owners, and when either dies you want to kill it, I might do something like this:
template<class T>
struct shared_connection {
// by default, kill the connection if we are connected:
~shared_connection() {
sever();
}
// wrap a shared pointer to the connection data:
shared_connection( std::shared_ptr<T> p ):
ptr(std::make_shared<std::shared_ptr<T>>(std::move(p)))
{}
// create a new connection:
shared_connection fork() const {
return {ptr};
}
// an even more explicit move:
shared_connection transfer() {
return std::move(*this);
}
friend void swap( shared_connection& lhs, shared_connection& rhs ) {
std::swap( lhs.ptr, rhs.ptr );
}
// move only type:
shared_connection(shared_connection&& src)
{
swap(*this, src);
};
shared_connection& operator=(shared_connection&& src)
{
auto tmp = std::move(src);
swap(*this, tmp);
return *this;
};
// lock it for use. The connection can die
// but the data will persist until the lock ends:
std::shared_ptr<T> lock() const {
if (!ptr) return {}; // don't break if we have been abandon()ed
return atomic_load(ptr.get());
}
// do not kill the connection:
void abandon() {
ptr = {};
}
// kill the connection:
void sever() {
atomic_store(ptr.get(), std::shared_ptr<T>{});
abandon();
}
// please just lock instead of calling this:
explicit operator bool() const {
return (bool)lock();
}
private:
std::shared_ptr<std::shared_ptr<T>> ptr;
};
template<class T, class...Args>
shared_connection<T> make_shared_connection( Args&&... args ) {
return std::make_shared<T>(std::forward<Args>(args)...);
}
First, create a shared_connection
via a std::make_shared<T>
.
Then .fork()
it to the other party. When any of the shared_connection
s go away, the internal T
is destroyed unless one of the shared_connection
s have it .lock()
ed.
I think I got the atomic code right so it supports the two sides of the connection being in different threads. It should also support multi-party connections.
The first shared pointer represents the shared control over the unlocked lifetime of the connection. The second shared pointer represents the ability to persist the connection for the duration of a short operation.
.abandon()
permits a someone to disconnect without killing the connection. .sever()
lets you destroy the shared connection data without destroying the object.
Code not tested.
If you want each side of the connection to share ownership within a bunch of code over their end of the shared connection, do a shared pointer to that shared connection. Because nothing says loving like a shared pointer to a shared pointer to a shared pointer.
We could also avoid the possibility of the inner shared pointer "leaking" by blocking access to it within an applicator:
template<class F, class R=std::decay_t<std::result_of_t<F(T const&)>>>
std::optional<R> read( F&& f ) const {
auto p = lock();
if (!p) return {};
T const& t = *p;
return std::make_optional<R>( std::forward<F>(f)(t) );
}
template<class F, class R=std::decay_t<std::result_of_t<F(T&)>>>
std::optional<R> write( F&& f ) const {
auto p = lock();
if (!p) return {};
T& t = *p;
return std::make_optional<R>( std::forward<F>(f)(t) );
}
by replacing the public .lock()
with the above methods.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With