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

心不动则不痛 提交于 2021-02-08 09:07:20

问题


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
}

回答1:


Unique ownership is unique, full stop. One place owns this resource and will release it when that code so chooses.

Shared ownership is shared. Multiple places can own this resource, and the resource will only be released when all of them have done so. This is a binary state: either one place owns the resource or multiple places do.

Your ownership semantics are unique... except when they're not. And rules that work a certain way except when they don't are kind of problematic.

Now, your specific implementation is full of holes. shared/weak_ptr are all explicitly part of the interface of these types, so it is exceptionally easy to just get the shared_ptr out of a strong_ptr. If you have a weak_ptr to a strong_ptr (required for borrowed_ptr::borrow), then you could just lock it and get a shared_ptr.

But even if your interface were to properly hide all of this (that is, you make your own weak_ptr-equivalent type and you stop inheriting from shared_ptr), your API cannot stop someone from storing that borrowed_ptr anywhere they want. Oh sure, they can't change it later, but it's easy enough to store it in a class member at construction time or to heap allocate one or whatever.

So at the end of the day, locking a weak pointer still represents a claim of ownership. Therefore, the ownership semantics of your pointer stack is still shared; there's simply an API encouragement to not retain shared ownership for too long.

unique_ptr doesn't have "API encouragement"; it has API enforcement. That's what gives it unique ownership. C++ does not have a mechanism to create similar enforcement of the ownership semantics you want to create.

Encouragement may be useful on some level, but it'd probably be just as useful to just have borrowed_ptr as the encouragement for those who want to express that they are only claiming ownership temporarily. And just directly use shared/weak_ptr as normal otherwise. That is, your API should explicitly recognize that it is using shared ownership, so that nobody is fooled into thinking otherwise.



来源:https://stackoverflow.com/questions/59632415/is-it-possible-desirable-to-create-non-copyable-shared-pointer-analogue-to-en

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!