Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there a smart pointer type that requires multiple owners?

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.

like image 450
Martijn Courteaux Avatar asked Aug 24 '16 10:08

Martijn Courteaux


People also ask

What smart pointer is used to allow multiple ownership of a value in various threads?

Rc<T> , a reference counting type that enables multiple ownership.

Are shared pointers smart pointers?

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

When would you use a shared pointer?

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.


3 Answers

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.

like image 107
David Schwartz Avatar answered Nov 05 '22 05:11

David Schwartz


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.

like image 36
Herb Sutter Avatar answered Nov 05 '22 05:11

Herb Sutter


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_connections go away, the internal T is destroyed unless one of the shared_connections 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.

like image 36
Yakk - Adam Nevraumont Avatar answered Nov 05 '22 05:11

Yakk - Adam Nevraumont