Extending the use of shared_from_this() to Constructors (- and to destructors?)

1,414 views
Skip to first unread message

Jakob Riedle

unread,
Dec 2, 2016, 10:01:13 AM12/2/16
to ISO C++ Standard - Future Proposals
Hello Folks,

at work it happened to me the second time,
that I needed to construct a shared_ptr/weak_ptr to 'this' within the constructor.

This stackoverflow question made think about the situation from the viewpoint of realizability.

I thus went and implemented my own drop-in replacement for shared_ptr, weak_ptr and enable_shared_from_this.
It is linked here:

Sourceforge: shared_from_this_ctor

Outcome:
Obviously, enable_shared_from_this can allow the user to construct a shared_ptr
while still in its ctor and simply pass it the reference_count object that enable_shared_from_this holds to
the shared_ptr that the constructed object will be passed to (after construction). (What a sentence...)

I have tested my apporach and it just works as expected.
This was as far as I'm concerned even acomplished with no speed or size overhead.

My questions now are:
  • Has this topic been already discussed somewhere else? What outcome?
  • What do you think? Is such usage still dangerous given a very robust implementation with no overhead?
  • Is there any use case for it? At least I had it twice now and it looks like, that other people have it as well.
( I know that one could create something using weak_from_raw, but I have not really dug into it yet. )


Beyond: Extending the approach to destructors:
One could argue: "Why not extend it to dtors as well?"

The question is: Is there any use case for this situation?
Because the functionality is perfectly demonstrated in my Library as well.

It works, as long as all shared_/weak_ptrs that get constructed during the dtor
get destroyed before dtor is over.
No duplicate deletion whatsoever - all clean -> it works! (Sadly, allowing this introduces a little bit of overhead :-) )



I'd love to hear your thoughts!


Greetings from Munich,
Jakob

Nicol Bolas

unread,
Dec 2, 2016, 11:47:17 AM12/2/16
to ISO C++ Standard - Future Proposals
Your implementation implements this by having `shared_ptr`'s acquiring constructors check to see if the type inherits from `enable_shared_from_this`. If it does, then it tries to get a `shared_ptr` from that. If it can, then it uses that to initialize itself. Otherwise, it does the usual acquisition stuff, passing itself into `enable_shared_from_this`'s machinery.

This breaks `make/allocate_shared`'s ability to create the control block and the `T` object in the same allocation. Your version of these functions makes no attempt to do so, and there's no way to implement that. If a type has `enable_shared_from_this`, then it's possible that its constructor could create a new `shared_ptr` internally, which would allocate the control block. That's bad, since `make/allocate_shared` want to do that allocation. That's a big part of the point of these functions.

Personally, I don't like the idea of an object being able to claim ownership of itself, even before it has finished being constructed. That sounds very wrongly designed.

Jakob Riedle

unread,
Dec 2, 2016, 3:01:15 PM12/2/16
to ISO C++ Standard - Future Proposals
This breaks `make/allocate_shared`'s ability to create the control block and the `T` object in the same allocation. Your version of these functions makes no attempt to do so, and there's no way to implement that.

Apparently, this is a non-binding requirement of the standard. I think it can be relaxed without code breakage, right?
Apart from that, I'm not telling anyone to use my implementation, I'm just asking, whether people like or dislike the functionality at all.
 
Personally, I don't like the idea of an object being able to claim ownership of itself, even before it has finished being constructed. That sounds very wrongly designed.

To some extent I can understand your worries.
You might want to put it this way:
If you're not owned by any shared_ptr instance (yet), then you are effectively the owner of yourself.

If an instantiation of a class that derives from enable_shared_from_this is not put into a shared_ptr later on,
all of it's methods that use shared_from_this() will lead to UB as well.
This risk of unbound enable_shared_from_this instances, was a trade-off that has already been accepted by introducing enable_shared_from_this.

And please note, that in case the shared_ptrs, that have been constructed in the ctor, all get destroyed, the objects dtor will not be called, since it still 'owns itself'.


If a type has `enable_shared_from_this`, then it's possible that its constructor could create a new `shared_ptr` internally, which would allocate the control block.

The point of a possible implementation is, that enable_shared_from_this obviously has to allocate the control-block (reference-count-struct) and pass it to the shared_ptr once assigned.
But as already state above, the risk of unassigned instances was already taken
and
is assumed to not outweight the benefit that comes from allowing usage of shared_from_this.


Generally:
Please not just say...
  • it doesn't work. There is (nearly) always a solution, if you are just willing to find one.
  • it can be misused. That is always the case, always. The question is, whether the risk of misuse outweights the benefit?
    ...and as stated above, if they use shared_from_this on instances that will never be owned by a shared_ptr, that will be a problem in the constructor as well.
Dear Nicol, I'm not your enemy and I don't force you to replace your opinion with mine.
But I invite you to answer constructively.

Nicol Bolas

unread,
Dec 2, 2016, 4:07:12 PM12/2/16
to ISO C++ Standard - Future Proposals
On Friday, December 2, 2016 at 3:01:15 PM UTC-5, Jakob Riedle wrote:
This breaks `make/allocate_shared`'s ability to create the control block and the `T` object in the same allocation. Your version of these functions makes no attempt to do so, and there's no way to implement that.

Apparently, this is a non-binding requirement of the standard. I think it can be relaxed without code breakage, right?

In the sense that code will still function, sure.

But what about the sense of code doing what we've told everyone it does? That implementation note in the standard, where it says that implementations "should" only perform one allocation, is really important. Saying that this optimization is the primary reason why `allocate_shared` exists is not overstating things. Note that while we have `make_unique`, we don't have `allocate_unqiue`.

We gauge the quality of implementations based on them following that note. People both expect and rely upon this optimization. We do not want to break optimizations that people have been relying on, even though their code technically still works. Just as we do not want to make changes to `basic_string` that make small-string optimizations illegal (we broke COW strings because that's not an optimization).

Such things would not "break" code in the sense of making it non-functional, but it would certainly get people to stop using the standard library version.

This sort of thinking has been applied to other things in the standard library. If our `variant` type required implementers to double-buffer the internal data, certain people would never use it. For them, that's too much of a tradeoff for the minor benefit of exception safety. And you can hardly blame them for wanting to avoid it for that reason.

Take this optimization away, and lots of people with stop using the standard library shared_ptr in favor of either a home-grown shared_ptr or a home-grown intrusive_ptr. This is to nobody's benefit.

Apart from that, I'm not telling anyone to use my implementation, I'm just asking, whether people like or dislike the functionality at all.

My point wasn't that your `make_shared` implementation didn't do the right thing. It's that it cannot implement the optimization. And therefore, if we adopt this, then we are telling people that the optimization no longer can be used.

This is not a feature which is worth that cost. If you have need of it, you can use your own `shared_ptr` type that provides this functionality.

Personally, I don't like the idea of an object being able to claim ownership of itself, even before it has finished being constructed. That sounds very wrongly designed.

To some extent I can understand your worries.
You might want to put it this way:
If you're not owned by any shared_ptr instance (yet), then you are effectively the owner of yourself.

To own something, even with shared ownership, means that you can delete it (or with shared ownership, terminate your share of that ownership). But you cannot end the lifetime of an object who's lifetime has not started yet. And by C++'s rules, an object's lifetime does not begin until one of its constructors finishes.

Therefore, you cannot claim ownership of an object that is still being constructed. That would be a logical contradiction of one of these concepts.

So if you're claiming ownership of yourself in your own constructor, then either you're doing something very wrong, or my idea of the concept of "ownership" is confused.

If an instantiation of a class that derives from enable_shared_from_this is not put into a shared_ptr later on,
all of it's methods that use shared_from_this() will lead to UB as well.

Not anymore; C++17 made a change here. You will get a `bad_weak_ptr` exception if you call `shared_from_this` on an object which was not bound to a `shared_ptr`.
 
This risk of unbound enable_shared_from_this instances, was a trade-off that has already been accepted by introducing enable_shared_from_this.

My point is not specifically the possibility of misuse. For me, it is the logic of the situation: what does it mean to have ownership of an object that does not yet exist?
 
If a type has `enable_shared_from_this`, then it's possible that its constructor could create a new `shared_ptr` internally, which would allocate the control block.

The point of a possible implementation is, that enable_shared_from_this obviously has to allocate the control-block (reference-count-struct) and pass it to the shared_ptr once assigned.

That doesn't help `make/allocate_shared` to provide the required optimization. Who allocates it is ultimately irrelevant; what matters is that it gets allocated in the same memory as the type being constructed. And unless `enable_shared_from_this` actually has the control block within itself, there's no way to prevent the allocation of a separate control block.

As for what I just mentioned (`enable_shared_from_this` storing the control block), there was an extensive discussion about this sort of thing in another thread. While it may or may not be possible in a theoretical sense, it would certainly not be possible for `enable_shared_from_this` as it currently stands to do it. This would require using a new type, due in part to potentially having to give that type an allocator.
 
Generally:
Please not just say...
  • it doesn't work. There is (nearly) always a solution, if you are just willing to find one.
  • it can be misused. That is always the case, always. The question is, whether the risk of misuse outweights the benefit?
    ...and as stated above, if they use shared_from_this on instances that will never be owned by a shared_ptr, that will be a problem in the constructor as well.
Dear Nicol, I'm not your enemy and I don't force you to replace your opinion with mine.
But I invite you to answer constructively.

What's unconstructive about what I said? I explained the problems I saw in the proposal: that it doesn't allow `make/allocate_shared` to do their job correctly, and that it supports a programming construct that I find to be highly dubious. You may not agree with my arguments or positions, but that hardly makes them unconstructive.

If you wish to prove point #1 wrong, all you need to do is provide an implementation where it can actually work. Just make sure that it works with both `make_shared` and `allocate_shared`. Which means you'll need to keep that allocator around and so forth.

Brittany Friedman

unread,
Dec 2, 2016, 4:26:04 PM12/2/16
to std-pr...@isocpp.org
There are proposals for an intrusively counted smart ptr:

I would imagine it much easier to get a retain_ptr to an object in its constructor when that object is responsible for its own reference count. This may be a viable alternative to trying to doing this for shared_ptr.

--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposals+unsubscribe@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/53300081-cbc4-43fc-806b-bccb3ef053b3%40isocpp.org.

Reply all
Reply to author
Forward
0 new messages