Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Another type of smart ptr, like unique_ptr with weak refs?

I recently hit an issue where neither unique_ptr nor shared_ptr seemed like the right solution. So, I am considering inventing another kind of smart ptr (described below), but I thought to myself "surely I am not the first to want this."

So my high-level questions are:

  • Does the below design make sense?
  • Is there some way to accomplish this with existing smart ptrs (or other std:: features), perhaps I am missing something?

Requirements:

  • I want single ownership, much like unique_ptr
    • That is: only when the single owning pointer dies, should the underlying object be freed (unlike shared_ptr's behavior).
  • I want some additional way to reference the object which is "aware" when the object gets deleted. So, something like weak_ptr, but to be used with a single ownership model.
  • I do not need thread safety

Motivating example:

Suppose I am iterating a list of interface pointers, calling methods on them. Some of those methods may result in items later in the list being deleted.

With plain pointers, I would get dangling references for those deleted items.

Proposed design:

Let's call the owning pointer my_ptr and the non-owning reference my_weak_ptr.

For a given object, we might have a diagram like this:

                             _______
my_ptr<Obj> owner ---------> |Obj* | -------> [Obj data ... ]
                      +----> |count|
                      | +--> |_____|
my_weak_ptr<Obj> A ---+ |
                        |
my_weak_ptr<Obj> B -----+

my_ptr would have an interface largely identical to unique_ptr. Internally, it would store a pointer to a "control block" which is really just the "real" pointer and a refcount for the control block itself. On destruction, my_ptr would set the control block pointer to NULL and decrement the refcount (and delete the control block, if appropriate).

my_weak_ptr would be copyable, and have some get() method which would return the real Obj*. The user would be responsible for checking this for NULL before using it. On destruction, my_weak_ptr would decrement the count (and delete the control block, if appropriate).

The downside is doing two hops through memory for each access. For my_ptr, this could be mitigated by storing the true Obj* internally as well, but the my_weak_ptr references will always have to pay that double-hop cost.


Edit: Some related questions, from links given:

  • Non-ownership copies of std::unique_ptr
  • Is it possible / desirable to create non-copyable shared pointer analogue (to enable weak_ptr tracking / borrow-type semantics)?
  • Better shared_ptr by distinct types for "ownership" and "reference"?

So it seems like there is demand for something like this, but no slam-dunk solutions. If thread safety is needed, shared_ptr and weak_ptr are the right choices, but if not, they add unnecessary overhead.

There is also boost::local_scoped_ptr, but it is still a shared ownership model; I'd rather prevent copies of the owning pointer, like unique_ptr.

like image 588
jwd Avatar asked May 12 '20 17:05

jwd


Video Answer


2 Answers

There was some good discussion in the comments above, so I'll try to answer my own question and summarize:

First, there is an overall downside to the whole concept: Any user of my_weak_ptr needs be very careful not to call some function which could result in the underlying object being deleted. Or if they do, they need to re-check the weak ptr for nullness. This is an unenforced (and unenforceable) constraint placed on the user, the same as if they were using raw pointers.

That being said: this is not new territory. In subsequent research, I have found various incarnations of such an idea:

  • VISH StrongPtr/WeakPtr
    • A pretty good fit. Note that WeakPtr has no lock() method, and the docs say "Weak pointers become magically null if the referred object is destroyed from elsewhere."
    • It is, however, still copyable, so does not express unique ownership.
  • Loki StrongPtr
    • With appropriate use of the dizzying choice of policies (or maybe a custom one), I think what I described can be accomplished.
  • trackable_ptr
    • A different approach, since your T must be wrapped as trackable<T>, but similar problem being solved.

There are also some "pretty good but not quite ideal" solutions closer to std:

  • Use shared_ptr and weak_ptr.
    • Downsides: thread safety overhead, copyable owner ptr, multiple ref counts.
  • boost::local_shared_ptr, which is compatible with weak_ptr.
    • Downsides: copyable owner ptr, multiple ref counts.

Probably local_shared_ptr is the best out-of-the-box solution, with high quality and few downsides.

However, to really squeeze out the last few bytes, and to disallow copying, a custom solution would be needed.

Aside, more philosophically:

I get the sense, both from discussion here and other reading, that many believe in a binary approach toward ownership: either it is shared (so use shared_ptr, which also gives you shared observation via weak_ptr) or it is unique (so use unique_ptr).

That probably covers a good 90%+ of cases. Yet I want unique ownership with shared observation (that's my phrasing; you might use different words depending on your semantics). Probably too corner-case to be covered by the standard, but I think it seems like a reasonable niche, for resource-constrained systems.

like image 197
jwd Avatar answered Oct 23 '22 05:10

jwd


There is a design which does not allocate extra block, but instead it makes pointer object sized as 3 pointers. A pointer is a node of double-linked list, each weak reference is a node of the same list.

Drawbacks are linear deletion complexity (must nullify each reference), and infeasibility making this efficiently thread safe.

Advantage is fast dereference of both shared and weak pointers.

I don't recall where exactly I've seen or heard this idea...

like image 29
Alex Guteniev Avatar answered Oct 23 '22 03:10

Alex Guteniev