mardi 7 janvier 2020

Is it possible / desirable to create non-copyable shared pointer analogue (to enable weak_ptr tracking / borrow-type semantics)?

Problem: Unique_ptrs express ownership well, but cannot have their object lifetimes tracked by weak_ptrs. Shared_ptrs can be tracked by weak_ptrs but do not express ownership clearly.

Proposed solution: Derive a new pointer type (I'm going to call it strong_ptr) that is simply a shared_ptr but with the copy constructor and assignment operator deleted, so that it is hard to clone them. We then create another new borrowed_ptr type (which is not easily storable) to handle the temporary lifetime extension required when the weak_ptr accesses the object, and can thereby avoid using shared_ptrs explicitly anywhere.

This question Non-ownership copies of std::unique_ptr adn this one Better shared_ptr by distinct types for "ownership" and "reference"? are both similar but in both cases the choice is framed as simply unique_ptr vs shared_ptr and the answer does not propose a satisfactory solution to my mind. (Perhaps I should be answering those questions instead of asking a new one? Not sure what the correct etiquette is in this case.)

Here's a basic stab. Note that in order to avoid the user of the weak pointer having to convert to shared_ptr to use it, I create a borrowed_ptr type (thanks rust for the name) which wraps shared_ptr but makes it hard for the user to accidentally store it. So by using differently hamstrung shared_ptr derivatives we can express the intended ownership and guide the client code into correct usage.

#include <memory>
template <typename T>
// This owns the memory
class strong_ptr : public std::shared_ptr<T> {
public:
  strong_ptr() = default;
  strong_ptr(T* t) : std::shared_ptr<T>(t) {}
  strong_ptr(const strong_ptr&) = delete;
  strong_ptr& operator=(const strong_ptr&) = delete;
};

template <typename T>
// This can temporarily extend the lifetime but is intentionally hard to store
class borrowed_ptr : public std::shared_ptr<T> {
public:
  borrowed_ptr() = delete;
  borrowed_ptr(const borrowed_ptr&) = delete;
  borrowed_ptr& operator=(const borrowed_ptr&) = delete;

  template <typename T>
  static borrowed_ptr borrow(const std::weak_ptr<T>& wp) 
  { 
    return wp.lock();
  }
private:
  borrowed_ptr(std::shared_ptr<T> &sp) : std::shared_ptr<T>(sp) {}
};

This seems fairly simple and an improvement over shared_ptr, but I cannot find any discussion of such a technique, so I can only imagine that I have missed an obvious flaw.

Can anyone give me a concrete reason why this is a bad idea? (And yes I know this is less efficient than unique_ptr - for PIMPL and so on I would still use unique_ptr.)

Caveat: I haven't yet used this in any more than a basic example, but this compiles and runs ok:

struct xxx
{
  int yyy;
  double zzz;
};

struct aaa
{
  borrowed_ptr<xxx> naughty;
};

void testfun()
{
  strong_ptr<xxx> stp = new xxx;
  stp->yyy = 123;
  stp->zzz = 0.456;

  std::weak_ptr<xxx> wkp = stp;

//  borrowed_ptr<xxx> shp = wkp.lock(); <-- Fails to compile as planned
//  aaa badStruct { borrowed_ptr<xxx>::borrow(wkp) }; <-- Fails to compile as planned
//  aaa anotherBadStruct; <-- Fails to compile as planned
  borrowed_ptr<xxx> brp = borrowed_ptr<xxx>::borrow(wkp); // Only way to create the borrowed pointer

//  std::cout << "wkp: " << wkp->yyy << std::endl; <-- Fails to compile as planned
  std::cout << "stp: " << stp->yyy << std::endl; // ok
  std::cout << "bp: " << brp->yyy << std::endl; // ok
}

Aucun commentaire:

Enregistrer un commentaire