1

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
}
void
  • 21
  • 3
  • 5
    "Shared_ptrs can be tracked by weak_ptrs but do not express ownership clearly." - I disagree. They clearly express shared ownership. – James Adkison Jan 07 '20 at 16:33
  • Don't you mean `unique_ptr`s? – rezaebrh Jan 07 '20 at 16:35
  • 1
    You can "borrow" from e.g. a `std::unique_ptr` easily by using `std::move`. The problem/disadvantage is that the compiler is not required to prevent you from using the moved-from object, and that there's similarly no proof that other parts of the code are not holding a reference/pointer to the moved-from object. The Rust compiler has these aspects built in, which limits the allowed code to a safer subset but makes some things more cumbersome to write. You can't fix these C++ compiler ""shortcomings"" with library classes. – Max Langhof Jan 07 '20 at 17:11
  • `Shared_ptrs ... do not express ownership clearly.` What do you mean? It's clear to me that they express shared ownership. It's unclear to me how this is an improvement over shared pointer. – eerorika Jan 07 '20 at 17:12
  • 1
    @eerorika: OP wants one `std::shared_ptr` as owned resource (and enforce that), several `std::weak_ptr` as safe observers. Alternative with `std::unique_ptr`and pointer doesn't provide the **safe** observer. – Jarod42 Jan 07 '20 at 17:22
  • @Jarod42 Why do they want that though? What is the use case? – eerorika Jan 07 '20 at 17:23
  • @eerorika: To have the better of the 2 worlds :) unique ownership is simpler, with the safety of `std::weak_ptr`. Possible usage would be to keep selected object in GUI. we don't want to extend lifetime of the object, but we want pointer to become `nullptr` when object is removed. In fact any possible usage of `std::observer_ptr` where lifetime of observed object might be shorter. – Jarod42 Jan 07 '20 at 17:32
  • @Jarod42 It's still unclear to me what "better" was gained from the world of unique ownership. As far as I can tell, we've only lost the ability to copy the ownership which appears to be a loss. You can already create weak pointers to shared pointers which have the desired behaviour. – eerorika Jan 07 '20 at 17:38
  • 1
    @eerorika: To express: "That class `C` owns the resources, you may view it, (but care, `C` can remove it anytime (I don't talk about threading))." so storing `weak_ptr` is fine. using **local** `shared_ptr` of `wptr.lock()` is fine too. but please don't store `shared_ptr`. – Jarod42 Jan 07 '20 at 17:47
  • @Jarod42 But why is it useful to not be able to store that `shared_ptr`? – eerorika Jan 07 '20 at 18:10
  • "To have the better of the 2 worlds :) unique ownership is simpler, with the safety of std::weak_ptr. Possible usage would be to keep selected object in GUI. we don't want to extend lifetime of the object, but we want pointer to become nullptr when object is removed." - This sounds like a Qt `QPointer` to me. – Jesper Juhl Jan 07 '20 at 18:35
  • 1
    @Jarod42 thanks you have expressed this better than I could have. I want to deter someone from storing multiple shared_ptrs to the same object. I think it is clearer to have a single long-term 'owner' with other observers that can only temporarily extend the object lifetime. Why don't I want multiple shared_ptrs? Because they can lead to us unintentionally prolonging the lifetime of an object, which is not much different to a memory leak in my opinion. You could simply do this with a coding convention, but I was interested as to whether I could enforce it with types instead. – void Jan 08 '20 at 12:30
  • 1
    @eerorika One specific use case I was envisaging is a kind of lazy subscription mechanism. There is a single defined owner of an object than controls the lifetime, and observers that need access to it but also need to know when it is deleted so that they can take appropriate action. Shared_ptr and weak_ptr allow for that, but I just wanted to create something that expressed the intention a little more clearly. – void Jan 08 '20 at 12:53

1 Answers1

4

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.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982
  • Note that an `optional` makes it completely trivial to create a regular pseudo-borrowed-pointer. Irregularity is pretty easy to erase. (with `emplace` tricks, you also avoid having to use any copy ctors when creating it in the optional as well; `template struct maker:std::function{ operator T() { return (*this)(); } };` is a toy one that lets you emplace anything you can construct into an optional.) – Yakk - Adam Nevraumont Jan 08 '20 at 02:27
  • @Yakk-AdamNevraumont: How do you get around the factory function? `emplace` only works through constructors, while `borrowed_ptr` is only created via a factory function. – Nicol Bolas Jan 08 '20 at 02:36
  • @NicolBolas thanks for the well-considered response; excellent criticism. I agree that I would need a wrapped weak_ptr to improve this, and that I cannot actually force the behaviour I want so if the user wants to circumvent the intention it is not difficult for them to do so. I also have some sympathy for the view that it hides the fact that someone other than the strong_ptr owner might be prolonging the lifetime of the object, which could be surprising, so maybe only the borrowed_ptr adds any value. – void Jan 08 '20 at 12:43
  • 1
    @nicol `optional.emplace( maker>{ [&]{ return borrowed_ptr::borrow(wp); } } )` – Yakk - Adam Nevraumont Jan 08 '20 at 14:47