std::make_shared is an optimization offered by the standard to allocate memory for an object and the shared_ptr 's state together.
std::enable_shared_from_this is a convenience offered by the standard to convert a naked pointer to a shared pointer.
Unless I misread the standard, their use together is not optimal, because the latter would still maintain a weak pointer to itself,
plus there is a potential with data races when shared_from_this() is used.
Is there any reason why the standard does not define a template like std::shared_ptr_state, that could be used just like std::enable_shared_from_this is used?
The benefit of std::shared_ptr_state is that a class derived from it would then contain its own shared state, thus dispensing with the need to have a weak pointer to itself, and that, I think, the data race could then be fully eliminated when calling shared_from_this().
--
--- You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Discussion" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-discussion+unsubscribe@isocpp.org.
To post to this group, send email to std-dis...@isocpp.org.
Visit this group at https://groups.google.com/a/isocpp.org/group/std-discussion/.
On Fri, Aug 5, 2016 at 6:34 PM, Nicol Bolas <jmck...@gmail.com> wrote:
> It's not a "convenience" since you wouldn't be able to implement it yourself.
I can implement it, and I am not willing to argue about the meaning of "convenience".
> It doesn't have to maintain a `weak_ptr` per-se. It simply maintains whatever state is needed; the *guts* of a `weak_ptr`. All it really needs is to bump the weak reference count when the object is first attached and to store a pointer to the shared state. The standard shows a `weak_ptr` being used as a "possible implementation", but it is hardly mandatory.
Hardly mandatory, bot MSVC and GCC both use a weak pointer.
> A data race with what? Can you provide some code examples of such a race?
I may have been mistaken here. I need to think more about it. Ditto for your allocator/deleter explanation.
On Fri, Aug 5, 2016 at 6:34 PM, Nicol Bolas <jmck...@gmail.com> wrote:> It's not a "convenience" since you wouldn't be able to implement it yourself.I can implement it, and I am not willing to argue about the meaning of "convenience".> It doesn't have to maintain a `weak_ptr` per-se. It simply maintains whatever state is needed; the *guts* of a `weak_ptr`. All it really needs is to bump the weak reference count when the object is first attached and to store a pointer to the shared state. The standard shows a `weak_ptr` being used as a "possible implementation", but it is hardly mandatory.Hardly mandatory, bot MSVC and GCC both use a weak pointer.
> A data race with what? Can you provide some code examples of such a race?I may have been mistaken here. I need to think more about it. Ditto for your allocator/deleter explanation.> Relative to a decent implementation of `enable_shared_from_this`, your suggested type would save you... one pointer of space.Not one, but two.
And CPU cycles spent on promoting weak to shared.
The issue is that shared_from_this would have to account for a T constructed on the stack, the heap, by an allocator, as a base class subobject, as a data member, and do something sensible in each case.
It might be possible to make it work in the make_shared case, but I have trouble seeing how to pass the information to the enable_shared_from_this base.
In contrast, intrusive pointers are well understood to solve this problem.
The difference is the footprint; the object pointer is unnecessary because the desired value is this.
What about an object under destruction?
On Fri, Aug 5, 2016 at 9:35 PM, Nicol Bolas <jmck...@gmail.com> wrote:> Sure, but that's a quality of implementation issue, not something the standard library can mandate.[...]> Nope; you'd have to do that work anyway. Whether the object holds the actual shared state or just a pointer to the shared state, it must do all of the work that `weak_ptr<T>::lock` must does in order to generate that `shared_ptr<T>`.So you're saying that using weak_ptr is QoI, while saying all that it implies must be done anyway. I'd suggest you think again.
If an object with an intrusive shared pointer is being destructed, shared_from_this has to fail as otherwise you have a dangling shared pointer afterwards. But that means that shared_from_this has to know that the object is being destructed, so it has to check the use count.
On 6 Aug 2016 2:54 p.m., "Nicol Bolas" <jmck...@gmail.com> wrote:
>
> Wait, I just realized something. You cannot embed the shared state in the object being managed. If you did, `weak_ptr` wouldn't be able to function at all.
>
> Weak_ptr works by referencing the shared state for the managed object. And it has to be able to access the shared state even when the object being managed has been destroyed. But if the object being managed contains the shared state, then the shared state will be destroyed right along with the object. The storage may not be, but you would be accessing an object who's lifetime has ended.
>
> The reason `make_shared` works is because the shared state exists outside the object being managed. The shared state is within the same allocation, but they are not in the same object; the shared state is not a subobject of the managed object.
>
> You cannot have weak_ptr work and have the shared state be a subobject of the managed object.
>
You can have the weak state only be a separate allocation; that means that if you never take a weak pointer you could avoid that allocation, and if you don't want weak pointers at all you can avoid the overhead of the pointer to weak state. I have a vague recollection that the boost intrusive shared pointer might do this.
On the
contrary: depending on what the object does when the refcount reaches
zero you can do only a subset of the things you listed as soon as *any*
intrusive_ptr to the object exists that might cause the refcount to
reach zero. It all depends on what assumptions the deleting operation
has on how and where the object was supposed to come into existence.
With shared_ptr_state it is the same, we just give it a name and make it
compatible with shared_ptr. Just instantiating the object as a member
works fine as long as no shared_ptr points to it. Otherwise you run into
exactly the same problems as with any other intrusive pointer if the
object wasn't designed for such a use case. Make shared_ptr_state accept
a custom deleter that only fires when the refcount goes to zero for the
extra kick.
As to the weak_ptr issue: shared_ptr_state<T, D> is part of the implementation and as such it is allowed to do what would otherwise be considered UB, including accessing the weak count member even though the containing object has already ended its lifetime using the appropriate compiler intrinsics/annotations or simply by knowing how the compiler works.
Now, you might say that if `A` were user-provided, then clearly the shared state can't be a base class of `T`. So on the user end, if a class derives from this state object, then `A` will never exist. So the actual user-facing shared state object would be the internal shared state, which uses an empty default allocator that would never be used.
So... what should happen if someone calls `alloc_shared<T>` on such a type? If the user calls one of the `shared_ptr` constructors that takes an allocator, then one could conceivably just ignore the allocator, since the storage for the shared state already exists. But with `alloc_shared`, no storage exists for anything yet. `alloc_shared` is required to allocate from that allocator, store the allocator within that storage, and use that stored copy of the allocator to delete it.
Is there some form of type-erasure that could handle this without requiring further allocations? Or would you just make it an error to call `alloc_shared` on such a type?
As to the weak_ptr issue: shared_ptr_state<T, D> is part of the implementation and as such it is allowed to do what would otherwise be considered UB, including accessing the weak count member even though the containing object has already ended its lifetime using the appropriate compiler intrinsics/annotations or simply by knowing how the compiler works.
Are there any other types, outside of Chapters 18 and a bit of 19, which have such an explicit reliance on compiler magic? Are there any other types where implementing them requires direct subversion of the C++ object and lifetime model? We're not talking about a type that, by its very nature, has to directly talk to the system (threads, IO, etc) or the compiler (type traits, etc). We're talking about a mere smart pointer.
It's one thing to have a type like `tuple`, where a clever compiler intrinsic or something could improve an implementation, but you can still implement it effectively otherwise. It's quite another thing altogether to define a type in such a way where it is functionally impossible to write an implementation that doesn't rely on platform-specific behavior.
`shared_ptr` actually has to store and type-erase a `shared_ptr_state<T, D, A>`, with `A` being the allocator used to allocate it the memory for it.
On 6 August 2016 at 14:09, Nicol Bolas <jmck...@gmail.com> wrote:`shared_ptr` actually has to store and type-erase a `shared_ptr_state<T, D, A>`, with `A` being the allocator used to allocate it the memory for it.No it does not. shared_ptr never does any allocations after construction time.
For example, using an aligned_storage buffer and neglecting to run the destructor on the contained A. Interesting; that could almost be legal.
>>
>> Now, you might say that if `A` were user-provided, then clearly the shared state can't be a base class of `T`. So on the user end, if a class derives from this state object, then `A` will never exist. So the actual user-facing shared state object would be the internal shared state, which uses an empty default allocator that would never be used.
>>
>> So... what should happen if someone calls `alloc_shared<T>` on such a type? If the user calls one of the `shared_ptr` constructors that takes an allocator, then one could conceivably just ignore the allocator, since the storage for the shared state already exists. But with `alloc_shared`, no storage exists for anything yet. `alloc_shared` is required to allocate from that allocator, store the allocator within that storage, and use that stored copy of the allocator to delete it.
>>
>> Is there some form of type-erasure that could handle this without requiring further allocations? Or would you just make it an error to call `alloc_shared` on such a type?
>
> If I call `allocate_shared()` I expect it to do what `allocate_shared()` does, and that is to not care about the nature of `T` and just do the normal business it does today. You don't seem to understand that the purpose of having `shared_ptr_state` accessible is to do these things *yourself* as is the point of having intrusive pointers in the first place. So If I decide to derivce my class from `shared_ptr_state<T, D, A>` I explicitly buy into managing its allocation myself and therefore can provide the allocator it needs.
>
>>
>>> As to the weak_ptr issue: shared_ptr_state<T, D> is part of the implementation and as such it is allowed to do what would otherwise be considered UB, including accessing the weak count member even though the containing object has already ended its lifetime using the appropriate compiler intrinsics/annotations or simply by knowing how the compiler works.
>>
>>
>> Are there any other types, outside of Chapters 18 and a bit of 19, which have such an explicit reliance on compiler magic? Are there any other types where implementing them requires direct subversion of the C++ object and lifetime model? We're not talking about a type that, by its very nature, has to directly talk to the system (threads, IO, etc) or the compiler (type traits, etc). We're talking about a mere smart pointer.
>>
>> It's one thing to have a type like `tuple`, where a clever compiler intrinsic or something could improve an implementation, but you can still implement it effectively otherwise. It's quite another thing altogether to define a type in such a way where it is functionally impossible to write an implementation that doesn't rely on platform-specific behavior.
>
> Honestly I don't care. The standard only describes how types behave, not whether they need to rely on compiler magic to realize their contract. And you would be surprised how many intrinsics are used throughout std implementations in places one doesn't necessarily expect. One doesn't need special rules to disable automatically running the allocator and ref counter destructors. One only needs to make sure the compiler does not remove access to these memory locations or overwrite them with magic values like DEADBEEF in debug builds until they are actually deallocated. Sure, an implementation that cannot do that needs a separate allocation but I would consider that a QoI problem.
>
> --
>
> ---
> You received this message because you are subscribed to a topic in the Google Groups "ISO C++ Standard - Discussion" group.
> To unsubscribe from this topic, visit https://groups.google.com/a/isocpp.org/d/topic/std-discussion/WF0gyN9g2mQ/unsubscribe.
> To unsubscribe from this group and all its topics, send an email to std-discussio...@isocpp.org.
Am 06.08.2016 um 21:09 schrieb Nicol Bolas:
It is trivial to construct a `container<A>` that stores a copy of `A` in a way that does not run its destructor when the enclosing object's destructor runs.On Saturday, August 6, 2016 at 12:26:31 PM UTC-4, Miro Knejp wrote:Am 06.08.2016 um 18:14 schrieb Nicol Bolas:
shared_ptr does not have *one* type of shared state but basically an infinite number since it performs type erasure. So in reality there actually *is* a shared_ptr_state<T, D> (among others) but shared_ptr itself only accesses the erased base type. Make the user-accessible shared_ptr_state<T, D> derive from the same base and you can make it compatible.Here's the thing. The weak_ptr issue makes it impossible for `shared_ptr_state<T, D>` to be the exact same type that `shared_ptr` and `make_shared` would normally have allocated. This is because weak_ptr would have to have a separate allocation for the weak reference count, so `shared_ptr_state` wouldn't have a weak reference count.
As such, `shared/weak_ptr` now have to basically have separate class implementations: one for objects that don't inherit from `shared_ptr_state`, and one for types that do. And it can't even use SFINAE/concepts to make that determination, since the `T` is not the managed object type. So instead, it has to be a runtime check, used by every function that touches the managed state.
All to save the overhead of one pointer (on good implementations. Two worst-case). If you need an overhead-less smart pointer, use an intrusive pointer. We should have such a type in the standard. But we should not transform `shared/weak_ptr` into such a type.
`shared_ptr` actually has to store and type-erase a `shared_ptr_state<T, D, A>`, with `A` being the allocator used to allocate it the memory for it. I bring that up because that throws another snag in your plan. Namely, that `A` is both (potentially) user-provided and stored within that object. An object which will be destroyed potentially well before the memory is deallocated. And since the allocator is a potentially user-provide class, you can't rely on compiler magic to make it work even though it is destroyed.
If I call `allocate_shared()` I expect it to do what `allocate_shared()` does, and that is to not care about the nature of `T` and just do the normal business it does today. You don't seem to understand that the purpose of having `shared_ptr_state` accessible is to do these things *yourself* as is the point of having intrusive pointers in the first place. So If I decide to derivce my class from `shared_ptr_state<T, D, A>` I explicitly buy into managing its allocation myself and therefore can provide the allocator it needs.Now, you might say that if `A` were user-provided, then clearly the shared state can't be a base class of `T`. So on the user end, if a class derives from this state object, then `A` will never exist. So the actual user-facing shared state object would be the internal shared state, which uses an empty default allocator that would never be used.
So... what should happen if someone calls `alloc_shared<T>` on such a type? If the user calls one of the `shared_ptr` constructors that takes an allocator, then one could conceivably just ignore the allocator, since the storage for the shared state already exists. But with `alloc_shared`, no storage exists for anything yet. `alloc_shared` is required to allocate from that allocator, store the allocator within that storage, and use that stored copy of the allocator to delete it.
Is there some form of type-erasure that could handle this without requiring further allocations? Or would you just make it an error to call `alloc_shared` on such a type?
Honestly I don't care. The standard only describes how types behave, not whether they need to rely on compiler magic to realize their contract. And you would be surprised how many intrinsics are used throughout std implementations in places one doesn't necessarily expect. One doesn't need special rules to disable automatically running the allocator and ref counter destructors. One only needs to make sure the compiler does not remove access to these memory locations or overwrite them with magic values like DEADBEEF in debug builds until they are actually deallocated. Sure, an implementation that cannot do that needs a separate allocation but I would consider that a QoI problem.As to the weak_ptr issue: shared_ptr_state<T, D> is part of the implementation and as such it is allowed to do what would otherwise be considered UB, including accessing the weak count member even though the containing object has already ended its lifetime using the appropriate compiler intrinsics/annotations or simply by knowing how the compiler works.
Are there any other types, outside of Chapters 18 and a bit of 19, which have such an explicit reliance on compiler magic? Are there any other types where implementing them requires direct subversion of the C++ object and lifetime model? We're not talking about a type that, by its very nature, has to directly talk to the system (threads, IO, etc) or the compiler (type traits, etc). We're talking about a mere smart pointer.
It's one thing to have a type like `tuple`, where a clever compiler intrinsic or something could improve an implementation, but you can still implement it effectively otherwise. It's quite another thing altogether to define a type in such a way where it is functionally impossible to write an implementation that doesn't rely on platform-specific behavior.
On Saturday, August 6, 2016 at 3:40:19 PM UTC-4, Miro Knejp wrote:Am 06.08.2016 um 21:09 schrieb Nicol Bolas:
It is trivial to construct a `container<A>` that stores a copy of `A` in a way that does not run its destructor when the enclosing object's destructor runs.On Saturday, August 6, 2016 at 12:26:31 PM UTC-4, Miro Knejp wrote:Am 06.08.2016 um 18:14 schrieb Nicol Bolas:
shared_ptr does not have *one* type of shared state but basically an infinite number since it performs type erasure. So in reality there actually *is* a shared_ptr_state<T, D> (among others) but shared_ptr itself only accesses the erased base type. Make the user-accessible shared_ptr_state<T, D> derive from the same base and you can make it compatible.Here's the thing. The weak_ptr issue makes it impossible for `shared_ptr_state<T, D>` to be the exact same type that `shared_ptr` and `make_shared` would normally have allocated. This is because weak_ptr would have to have a separate allocation for the weak reference count, so `shared_ptr_state` wouldn't have a weak reference count.
As such, `shared/weak_ptr` now have to basically have separate class implementations: one for objects that don't inherit from `shared_ptr_state`, and one for types that do. And it can't even use SFINAE/concepts to make that determination, since the `T` is not the managed object type. So instead, it has to be a runtime check, used by every function that touches the managed state.
All to save the overhead of one pointer (on good implementations. Two worst-case). If you need an overhead-less smart pointer, use an intrusive pointer. We should have such a type in the standard. But we should not transform `shared/weak_ptr` into such a type.
`shared_ptr` actually has to store and type-erase a `shared_ptr_state<T, D, A>`, with `A` being the allocator used to allocate it the memory for it. I bring that up because that throws another snag in your plan. Namely, that `A` is both (potentially) user-provided and stored within that object. An object which will be destroyed potentially well before the memory is deallocated. And since the allocator is a potentially user-provide class, you can't rely on compiler magic to make it work even though it is destroyed.
Sure, but how do you get the pointer to `A` back? And you have to be able to do that from just a `weak_ptr`.
If I call `allocate_shared()` I expect it to do what `allocate_shared()` does, and that is to not care about the nature of `T` and just do the normal business it does today. You don't seem to understand that the purpose of having `shared_ptr_state` accessible is to do these things *yourself* as is the point of having intrusive pointers in the first place. So If I decide to derivce my class from `shared_ptr_state<T, D, A>` I explicitly buy into managing its allocation myself and therefore can provide the allocator it needs.Now, you might say that if `A` were user-provided, then clearly the shared state can't be a base class of `T`. So on the user end, if a class derives from this state object, then `A` will never exist. So the actual user-facing shared state object would be the internal shared state, which uses an empty default allocator that would never be used.
So... what should happen if someone calls `alloc_shared<T>` on such a type? If the user calls one of the `shared_ptr` constructors that takes an allocator, then one could conceivably just ignore the allocator, since the storage for the shared state already exists. But with `alloc_shared`, no storage exists for anything yet. `alloc_shared` is required to allocate from that allocator, store the allocator within that storage, and use that stored copy of the allocator to delete it.
Is there some form of type-erasure that could handle this without requiring further allocations? Or would you just make it an error to call `alloc_shared` on such a type?
OK, so you're saying that if you have some type that derives from shared_ptr_state, you want it to fail if the user tries to provide a different deleter or allocator type than the one in the object already. You want `allocate_shared` to fail if you provide a different allocator, and you want the `shared_ptr` constructors to fail if you provide a different deleter/allocator.
You've yet to answer the critical question: why do you need `shared_ptr` to do all of this? If you want to make an `intrusive_ptr` type that's based on some `instrusive_state`, why not propose that? Why do we need to transform `shared_ptr` into an intrusive pointer? Because `enable_shared_from_this` is a pointer or two larger than it absolutely needs to be?
Not only that, such an intrusive_ptr wouldn't even be as good as a real intrusive_ptr. Why? Because in a real intrusive_ptr, it would allow you to manage the state, rather than forcing you to derive from a class and break standard layout. Just look at `boost::intrusive_ptr`. It calls your code; it doesn't force you to use its class and derive from it. If you don't want to use atomic increments/decrements, you don't have to.
Why do you want to settle for this overcomplicated, half-formed intrusive_ptr-that-pretends-its-not instead of using the real thing?
Honestly I don't care. The standard only describes how types behave, not whether they need to rely on compiler magic to realize their contract. And you would be surprised how many intrinsics are used throughout std implementations in places one doesn't necessarily expect. One doesn't need special rules to disable automatically running the allocator and ref counter destructors. One only needs to make sure the compiler does not remove access to these memory locations or overwrite them with magic values like DEADBEEF in debug builds until they are actually deallocated. Sure, an implementation that cannot do that needs a separate allocation but I would consider that a QoI problem.As to the weak_ptr issue: shared_ptr_state<T, D> is part of the implementation and as such it is allowed to do what would otherwise be considered UB, including accessing the weak count member even though the containing object has already ended its lifetime using the appropriate compiler intrinsics/annotations or simply by knowing how the compiler works.
Are there any other types, outside of Chapters 18 and a bit of 19, which have such an explicit reliance on compiler magic? Are there any other types where implementing them requires direct subversion of the C++ object and lifetime model? We're not talking about a type that, by its very nature, has to directly talk to the system (threads, IO, etc) or the compiler (type traits, etc). We're talking about a mere smart pointer.
It's one thing to have a type like `tuple`, where a clever compiler intrinsic or something could improve an implementation, but you can still implement it effectively otherwise. It's quite another thing altogether to define a type in such a way where it is functionally impossible to write an implementation that doesn't rely on platform-specific behavior.
It can't be a QoI problem; weak_ptr's constructors are `noexcept`, so they're not allowed to allocate memory.
Changing that is a breaking change. Furthermore, even if we were OK with such a change, we would also need to expand `weak_ptr`'s set of constructors, so that you could feed them an allocator to manage any memory they choose to allocate. And God only knows what happens if you pass a different allocator type to different `weak_ptr` constructors.
Not caring about these problems doesn't make them go away.
Am 06.08.2016 um 23:48 schrieb Nicol Bolas:
On Saturday, August 6, 2016 at 3:40:19 PM UTC-4, Miro Knejp wrote:Am 06.08.2016 um 21:09 schrieb Nicol Bolas:
It's not about making `shared_ptr` intrusive but about having the option for it to be so if the situation calls for it.You've yet to answer the critical question: why do you need `shared_ptr` to do all of this? If you want to make an `intrusive_ptr` type that's based on some `instrusive_state`, why not propose that? Why do we need to transform `shared_ptr` into an intrusive pointer? Because `enable_shared_from_this` is a pointer or two larger than it absolutely needs to be?
`shared_ptr` should not care about how its control block or the object were allocated as long as it conforms to the protocol `shared_ptr` establishes with its shared state. If I create an object that contains the shared state, create a `shared_ptr` form the `shared_ptr_state` and then pass said `shared_ptr` to the rest of the program I expect it to work. Using `shared_ptr_state` implies that one understands they are dealing with a low-level facility that has implications on how the type interacts with other parts of the library. To me it is obvious that if I create the `shared_ptr_state` as part of my object I have to use it as a factory to get the first `shared_ptr` instance out of it. Anything else just doesn't make sense and cannot possibly work. If I create the object and then construct a `shared_ptr` with a pointer I expect it to have a constructor that accepts `shared_ptr_state*` and deals with it appropriately.
That behavior can be extrapolated to `make_shared()` and `allocate_shared()` as necessary.
It isn't any different from how a type using intrusive_ptr behaves. If a type is hardcoded to use a certain allocator then guess what, you cannot allocate it in a differnt way or things fall apart when the intrusive ref count goes to zero. You keep bringin up problems that are also problems with intrusive pointers.
Because this is not about getting an intrusive_ptr it's about having control over shared_ptr's control block management. It'd be great to have a dedicated `intrusive_ptr` but that won't be compatible with `shared_ptr` without having access to a facility like the one proposed here. Obviously if you don't need to be compatible with `shared_ptr` then you won't care about `shared_ptr_state`.Why do you want to settle for this overcomplicated, half-formed intrusive_ptr-that-pretends-its-not instead of using the real thing?
Right now I cannot safely create an object on stack/array/subobject/etc and create a `shared_ptr` to it without at least allocating the shared state with a null-allocator or requiring the enclosing scope to be managed by a `shared_ptr` already.
Let `shared_ptr_state` allocate the memory if needed. Nobody except you is suggesting `weak_ptr`c needs to allocate something. So yes, it is a QoI issue.Honestly I don't care. The standard only describes how types behave, not whether they need to rely on compiler magic to realize their contract. And you would be surprised how many intrinsics are used throughout std implementations in places one doesn't necessarily expect. One doesn't need special rules to disable automatically running the allocator and ref counter destructors. One only needs to make sure the compiler does not remove access to these memory locations or overwrite them with magic values like DEADBEEF in debug builds until they are actually deallocated. Sure, an implementation that cannot do that needs a separate allocation but I would consider that a QoI problem.
It can't be a QoI problem; weak_ptr's constructors are `noexcept`, so they're not allowed to allocate memory.
On Sat, Aug 6, 2016 at 3:54 PM, Nicol Bolas <jmck...@gmail.com> wrote:> Wait, I just realized something. You cannot embed the shared state in the object being managed. If you did, `weak_ptr` wouldn't be able to function at all.You may be mistaken about the "function" of weak_ptr per the standard. You assume, as you said in a later message, that the shared state is supposed to outlive the object, with a further assumption that as soon as the state is only referenced by weak_ptr's, the object is deleted and storage deallocated. I do not think this is required by the standard and I know that in certain implementations make_shared just allocates everything in one big happy blob.
On 08/08/16 18:18, Andrey Semashev wrote:
On 08/08/16 14:58, Viacheslav Usov wrote:
On Mon, Aug 8, 2016 at 1:34 PM, 'Edward Catmur' via ISO C++ Standard -
Discussion <std-dis...@isocpp.org
<mailto:std-discussion@isocpp.org>> wrote:
If the weak state is included in the shared state, and the sharedstate is included in the object (the intent of inheriting from
shared_ptr_state) then the weak state will be destroyed when the object
is destroyed, so any weak pointers will point to destroyed state; this
would make it difficult to implement without relying on undefined or at
least implementation-specific behavior to make referring to destroyed
state OK.
Why does this need to invoke undefined or implementation-specific
behaviour? If the storage for the object is not deleted, then the only
effect is that of running the destructor.
Running the body of the destructor may not be the only thing the
compiler has to do. For example, the compiler also has to ensure the
correct virtual functions are called during destruction (i.e. update the
pointer to the vtable). As John has mentioned, the compiler is also not
forbidden to clobber memory after destruction (e.g. fill with trapping
values).
I think [basic.life]/6 is pretty clear on this:
... The program has undefined behavior if:
...
— the glvalue is used to access a non-static data member or call a
non-static member function of the object, or
...
Also, if the allocator is destroyed while the object is destroyed, arguably it will not be able to deallocate the storage.The memory location is not magically invalidated or made untouchable
by that.
It's not, but it no longer represents the object. It is raw storage with
unspecified contents, nothing more.
If the destructor of
the shared state is trivial, then, per [basic.life] it ends its lifetime
when the storage is reused or released. And because the embedded shared
state does not need much more than two reference counts, it is very hard
to imagine why its destructor would be non-trivial.
First, the enclosing object's destructor can be non-trivial. Second, the
shared state may contain a deleter and an allocator that are supposed to
be used to destroy the object and deallocate the storage. Either of
these types may have non-trivial destructors.
We aren't accessing a non-static data member, we're accessing a
non-static data member of a non-static data member.
I don't see the significance. If an object is destroyed then all its non-static members are also destroyed, recursively so.
I'm considering the case when all shared_ptrs to the object are destroyed and weak_ptrs are left referencing the storage which is supposed to hold the shared_ptr_state. These weak_ptrs will have to access shared_ptr_state, and in order for that to be well-defined the state has to be not destroyed. It cannot be so if the state is a member of the object, no matter at what level of object nesting.
If we construct placement-construct allocator and/or deleter in raw
storage contained within the complete object, we can defer destruction
of the allocator and/or deleter until the weak state is disposed.
I don't think this is a valid assumption. If the enclosing class' destructor is not trivial, you cannot assume anything about the contents of that raw storage after the destructor completes.
As John has mentioned, the compiler is also not forbidden to clobbermemory after destruction (e.g. fill with trapping values).
I hope he will be able to substantiate that by quoting the standard.
I don't think he has to. The standard doesn't specify the contents of the storage after the non-trivial constructor completes, which means the contents are, well, unspecified.
On Mon, Aug 8, 2016 at 6:16 PM, Nicol Bolas <jmck...@gmail.com> wrote:> Your statement is rather like saying, "if the Grand Canyon weren't in the way, we could just walk there, right?" That's true... but it's not particularly useful.That is useful, and if you think about it, you will certainly remember that as a programmer you must have done that sort of thing just about every day.In this particular case, it is useful, because if it is only weak_ptr that is a problem, it is not a fundamental problem, without even going into all those complications we have been talking about recently.Each weak_ptr can have a field that links it to another weak_ptr. The shared state can have a field that is the head of the list of all the weak_ptr's to the given shared state. The implementation can have a global mutex that synchronises access to all the weak_ptr lists. When a shared state goes down, it simply iterates over all the weak_ptrs that point to it, and makes their pointers to itself null.
On segunda-feira, 8 de agosto de 2016 16:36:08 PDT 'Edward Catmur' via ISO C++
Standard - Discussion wrote:
> But it is forbidden to clobber memory of a trivially destructible complete
> object, right?
The trivial destructor can't do it, clearly, otherwise it wouldn't be trivial.
But a non-trivial destructor for an outer object that contains this sub-object
could.
On segunda-feira, 8 de agosto de 2016 17:39:51 PDT 'Edward Catmur' via ISO C++
Standard - Discussion wrote:
> On Mon, Aug 8, 2016 at 5:07 PM, Andrey Semashev <andrey....@gmail.com>
> wrote:
> >> We aren't accessing a non-static data member, we're accessing a
> >> non-static data member of a non-static data member.
> >
> > I don't see the significance. If an object is destroyed then all its
> > non-static members are also destroyed, recursively so.
>
> That doesn't necessarily mean their lifetime has ended. Yes, non-trivial
> destructors are run, and those end the lifetime of the objects they are run
> on, but only up until a trivially destructible subobject is reached.
Sorry, that doesn't make sense. It has to mean that. When the lifetime of an
object ends, the lifetime of all of its sub-objects, recursively, must have
ended too.
If you meant that the memory continues to be comprised of bytes and those
bytes can be read, sure, they can. It doesn't mean that they continue to
contain what they used to. The contents may have changed because of the non-
trivial destructors that were run.
> > I don't think this is a valid assumption. If the enclosing class'
> > destructor is not trivial, you cannot assume anything about the contents
> > of
> > that raw storage after the destructor completes.
>
> Again, why? User code cannot touch that raw storage; where is the license
> for the compiler to do so? I thought [class.cdtor]/1 was such license, but
> Viacheslav has persuaded me that may not be so clear.
Why can't the user code touch that raw storage? If the sub-object is trivially
destructible, then it doesn't matter what bytes it contains when the that
destructor would have been run. So the user code *can* memset it and it's a
valid use of the sub-object.
Moreover, even if we say that the lifetime ends only when another object's
lifetime begins there, what's to prevent exactly that from happening? Why
couldn't the non-trivial outer destructor begin the lifetime of another sub-
object in the same storage bytes?
> > As John has mentioned, the compiler is also not forbidden to clobber
> >> memory after destruction (e.g. fill with trapping values).
> >>
> >> I hope he will be able to substantiate that by quoting the standard.
> >
> > I don't think he has to. The standard doesn't specify the contents of the
> > storage after the non-trivial constructor completes, which means the
> > contents are, well, unspecified.
>
> Well, maybe it does specify the contents. If user code does not write to
> the storage during the non-trivial destructor sequence, and if the lifetime
> of the storage continues through the destructor call, and if it is legal to
> use a previously formed pointer or reference to access its contents, then
> its contents must be the same as before the destructor call, just as they
> would be were the complete object destructor trivial.
I disagree. There's no guarantee that the byte contents of a non-trivial
object will remain the same after said destructor has run.
Proof by absurd: you're saying that all trivial sub-objects of any object must
retain their values as they had before the outer object's destructor was run.
But obviously everything is constructed from primitive types, which are
trivial. The only way to do that is if destructors are not allowed to write to
any of their sub-objects. Conclusion: all destructors are trivial.
Are all destructors trivial?
On 08/08/16 19:39, 'Edward Catmur' via ISO C++ Standard - Discussion wrote:
On Mon, Aug 8, 2016 at 5:07 PM, Andrey Semashev
<andrey....@gmail.com <mailto:andrey.semashev@gmail.com>> wrote:
We aren't accessing a non-static data member, we're accessing a
non-static data member of a non-static data member.
I don't see the significance. If an object is destroyed then all its
non-static members are also destroyed, recursively so.
That doesn't necessarily mean their lifetime has ended. Yes, non-trivial
destructors are run, and those end the lifetime of the objects they are
run on, but only up until a trivially destructible subobject is reached.
I'm considering the case when all shared_ptrs to the object are
destroyed and weak_ptrs are left referencing the storage which is
supposed to hold the shared_ptr_state. These weak_ptrs will have to
access shared_ptr_state, and in order for that to be well-defined
the state has to be not destroyed. It cannot be so if the state is a
member of the object, no matter at what level of object nesting.
Why? The storage holding the shared state may be destroyed, but the
lifetime of the storage may not have ended. Its lifetime has certainly
not ended if the complete object is trivially destructible; at question
is whether its lifetime has ended when the complete object is not
trivially destructible.
The mentioned earlier [class.cdtor]/1 says that you can't access a non-static data member - any member - after the non-trivial destructor completed. It doesn't matter whether that member's destructor is trivial or not.
I understand what you're saying, and I do think there is slight inconsistency between this paragraph and lifetime rules for trivially destructible objects. But those inconsistencies only contribute to my opinion that accessing a trivially destructible member of a destroyed non-trivially destructible object is UB. And frankly, IMO it should be UB.
I think lifetime rules could be updated so that trivial constructors/destructors also act as points of starting ending the object lifetime. A separate paragraph could be added stating that the trivial constructors/destructors don't affect the underlying storage. It would still be UB to access a trivial member of a destroyed non-trivial object but at least the confusion wrt object lifetime would be resolved.
If we construct placement-construct allocator and/or deleter in raw
storage contained within the complete object, we can defer
destruction
of the allocator and/or deleter until the weak state is disposed.
I don't think this is a valid assumption. If the enclosing class'
destructor is not trivial, you cannot assume anything about the
contents of that raw storage after the destructor completes.
Again, why? User code cannot touch that raw storage; where is the
license for the compiler to do so? I thought [class.cdtor]/1 was such
license, but Viacheslav has persuaded me that may not be so clear.
I didn't say user can't touch the storage, he can. He just can't assume any particular contents in that storage.
The standard doesn't specify the contents
of the storage after the non-trivial constructor completes, which
means the contents are, well, unspecified.
Well, maybe it does specify the contents. If user code does not write to
the storage during the non-trivial destructor sequence, and if the
lifetime of the storage continues through the destructor call, and if it
is legal to use a previously formed pointer or reference to access its
contents, then its contents must be the same as before the destructor
call, just as they would be were the complete object destructor trivial.
Does the standard guarantee this anywhere? I didn't find any statements that would amount to that guarantee - on the countrary, the standard allows compilers a great deal of latitude in implementing constructors and destructors involving any kind of magic.
On segunda-feira, 8 de agosto de 2016 19:01:54 PDT 'Edward Catmur' via ISO C++
Standard - Discussion wrote:
> > The trivial destructor can't do it, clearly, otherwise it wouldn't be
> > trivial.
> >
> > But a non-trivial destructor for an outer object that contains this
> > sub-object
> > could.
>
> A complete object is not a sub-object. What did you mean here?
Each object may be a sub-object of a larger object.
The complete object wrapping sub-objects is allowed to do anything to the sub-
objects it is composed of.
On segunda-feira, 8 de agosto de 2016 19:02:05 PDT Viacheslav Usov wrote:
> > Sorry, it's very much relevant because I don't think the earlier
> > discussion's conclusion was valid. The whole object is responsible for its
> > storage and could overwrite any of the sub-objects. The destructor is
> > perfectly allowed to do that memset(this, 0xcd, sizeof(*this)); before
> > returning.
>
> Which is instant UB for all but POD and similar types. Consider:
>
> struct A
> {
> std::unique_ptr<char> ptr;
> ~A() { memset(this, 0xcd, sizeof(*this)); }
> };
>
> What happens to ptr which is yet to be destructed after the memset?
Fair enough, even though that's not what I meant. I meant that the compiler
could insert that after the sub-objects were destroyed, which wouldn't be UB.
That would happen after the unique_ptr destructor in your example. Nothing in
the standard prevents the compiler from doing that, which means it's
permissible.
But even then, we would still be able to memset the trivial sub-objects. Like
so:
struct B
{
some_trivial_object obj;
~B() { memset(&obj, 0xcd, sizeof(obj); }
};
Now rename some_trivial_object as shared_ptr_state.
> > The point is that the experience of something working in a given
> > situation is not guarantee it will always work, in other situations.
>
> I did not invoke my experience only. There were other reasons, which you
> keep ignoring.
I'm addressing the reasons. This point above was exclusively as a reply to
"And, really, if it is use after DELETION, why is it free do so after
DESTRUCTION? In my experience, that is done when memory is deallocated, not
when the object is destructed."
Your experience is biased towards the implementations you've got experience
with. You cannot claim experience as a rule for what is impossible.
#include <memory>struct X { int i = 42; };struct Y : X { ~Y() {} };int main() {alignas(Y) char buf[sizeof(Y)];Y* p = new (buf) Y;int* p1 = &p->i;p->~Y();return *p1; // ???}
On 08/08/16 21:48, 'Edward Catmur' via ISO C++ Standard - Discussion wrote:
On Mon, Aug 8, 2016 at 6:17 PM, Andrey Semashev
<andrey....@gmail.com <mailto:andrey.semashev@gmail.com>> wrote:
The mentioned earlier [class.cdtor]/1 says that you can't access a
non-static data member - any member - after the non-trivial
destructor completed. It doesn't matter whether that member's
destructor is trivial or not.
Membership is not transitive. If we indirect a pointer to a non-direct
nested member (a subobject of a subobject), have we accessed the
intermediate member (the immediate subobject)?
How a sub-object can be destroyed while its sub-sub-object is not? Or are you saying that dereferencing a pointer to the destroyed sub-object is UB and at the same time dereferencing a pointer to the destroyed sub-sub-object is fine? I'm sorry, but neither of these make sense to me.
I didn't say user can't touch the storage, he can. He just can't
assume any particular contents in that storage.
Why not, if they ensure that the storage has particular contents at the
time the destructor completes?
As I said earlier, the user is not in position to ensure that unless he targets a particular implementation and that implementation allows that.
On segunda-feira, 8 de agosto de 2016 19:24:18 PDT 'Edward Catmur' via ISO C++
Standard - Discussion wrote:
> Where does the Standard say that nested subobject lifetimes are nested
> isomorphically?
It doesn't say that they aren't, which is why we're having this discussion in
the first place. So we have to look at this from the point of view of what was
intended and what makes most sense.
I submit that recusive ending of lifetime is the option that makes most sense.
The other option requires too many exceptions to be implementable. It would
only apply to:
a) non-public members
b) of trivially destructible classes
c) which are members of at most one non-trivial, outer object
The lifetime of all other members in such non-trivial classes would be
considered ended.
You're arguing that you could have taken a pointer to that memory
location and you could access it after the point where the trivial destructor
would have run. Well, if you can do that, then I can also do it from my own
destructor.
struct B;
struct A
{
B b;
std::shared_ptr_state state;
A() : state{}, b(&state) {}
};
struct B
{
void *ptr;
B(void *storage) : ptr(storage) {}
~B();
};
Since A::b's destructor is run after A::state, then I can use the storage area
in B's destructor. Whether A::state contained any non-public members is
irrelevant.
> > Why can't the user code touch that raw storage? If the sub-object is
> > trivially
> > destructible, then it doesn't matter what bytes it contains when the that
> > destructor would have been run. So the user code *can* memset it and it's
> > a
> > valid use of the sub-object.
> >
> > Moreover, even if we say that the lifetime ends only when another object's
> > lifetime begins there, what's to prevent exactly that from happening? Why
> > couldn't the non-trivial outer destructor begin the lifetime of another
> > sub-
> > object in the same storage bytes?
>
> That's trivial to guard against; contain the trivially destructible
> subobject in a non-trivially destructible subobject controlled by the
> library. The user cannot touch the storage of the interposed object because
> it is non-trivially destructible, and the interposed object non-trivial
> destructor chooses not to touch the storage, so the only agent that
> conceivably could touch the storage is the compiler.
That doesn't work. If you put the trivial state inside a non-trivial wrapper,
then the wrapper's non-trivial destructor is run and its lifetime is clearly
ended. That means the storage bytes are free to be reused again.
> > Proof by absurd: you're saying that all trivial sub-objects of any object
> > must
> > retain their values as they had before the outer object's destructor was
> > run.
> > But obviously everything is constructed from primitive types, which are
> > trivial. The only way to do that is if destructors are not allowed to
> > write to
> > any of their sub-objects. Conclusion: all destructors are trivial.
> >
> > Are all destructors trivial?
>
> I'm sorry, I can't follow your reasoning. I'm not saying that destructors
> aren't allowed to write to subobjects, I'm saying that if they choose not
> to write to those subobjects, then those subobjects are not otherwise
> altered. I explicitly mentioned that as my first condition: "If user code
> does not write to the storage during the non-trivial destructor sequence".
That's not what you're saying. Your previous claims are different than what
you've just written. You want to guarantee that the contents of a trivial sub-
object of any object remain constant after the destructor of that object has
finished. To do that, you need to guarantee all cases, not just if "they [the
destructors] choose not to write to those subobjects".
If you can't guarantee in all cases, then you must assume that they are
different. If they can be different, then the compiler is allowed to assume no
one is going to use that storage.
On segunda-feira, 8 de agosto de 2016 21:34:35 PDT 'Edward Catmur' via ISO C++
Standard - Discussion wrote:
> The fact that an object has been destroyed is not the definitive answer to
> whether it can be accessed. The Standard says when user-supplied
> destructors run and when the lifetime of complete objects and non-trivially
> destructible subobjects ends. It says that subobjects of non-trivially
> destructible objects cannot be accessed after the containing destructor has
> completed, but it does not say explicitly that sub-subobjects cannot be
> accessed. It does not say that the lifetime of trivially destructible
> subobjects is contained within the lifetime of their parent objects.
>
> There are two possibilities here: either the language is defective, and
> sub-subobjects cannot be accessed, or it is written as intended, and
> sub-subobjects can be accessed as long as the immediate subobject is not
> accessed, up until the storage of the complete object is released or
> reused. My intuition leans towards the former possibility, but only weakly.
I think we'll have to define what it means for the lifetime to end.
Each of the primitive types that compose the complete object that used to
exist continue to exist. They are still there. Such as in:
struct A
{
int i;
~A();
}:
a.~A();
a.i = 0; // begins new lifetime for A::i
However, their contents are all unspecified. Code cannot assume that values are
retained past destruction. Is that the same as the lifetime ending?
More importantly: what can the compiler assume about contents?
On segunda-feira, 8 de agosto de 2016 20:29:01 PDT 'Edward Catmur' via ISO C++
Standard - Discussion wrote:
> > Fair enough, even though that's not what I meant. I meant that the
> > compiler
> > could insert that after the sub-objects were destroyed, which wouldn't be
> > UB.
> > That would happen after the unique_ptr destructor in your example. Nothing
> > in
> > the standard prevents the compiler from doing that, which means it's
> > permissible.
>
> An object retains its value over its lifetime, so if the nested subobject
> outlives its complete object and can be observed to have done so the
> compiler is precluded from clobbering it.
A sub-object cannot outlive its containing object because, even if it could,
the contents are unspecified. I submit that "contents become unspecified" means
that the previous object's lifetime ended.
> > But even then, we would still be able to memset the trivial sub-objects.
> > Like
> > so:
> >
> > struct B
> > {
> > some_trivial_object obj;
> > ~B() { memset(&obj, 0xcd, sizeof(obj); }
> > };
> >
> > Now rename some_trivial_object as shared_ptr_state.
>
> shared_ptr_state does not need to be itself trivial, it just (possibly)
> needs to have a trivial (eventual) subobject.
You're just adding another level of indirection. If I took struct B above and
placed it in:
struct C { B b; };
Then both C and B have non-trivial destructors and the innermost trivial sub-
object cannot be guaranteed to retain its byte values. There's the case I had
above, plus one more: now we *know* that C::b's lifetime ended since the non-
trivial destructor has finished, so the storage bytes can be reused. The
compiler is free to use them as a scratch area for something.
> > Your experience is biased towards the implementations you've got
> > experience
> > with. You cannot claim experience as a rule for what is impossible.
>
> Experience can help, though. Consider:
>
> #include <memory>
>
> struct X { int i = 42; };
> struct Y : X { ~Y() {} };
>
> int main() {
> alignas(Y) char buf[sizeof(Y)];
> Y* p = new (buf) Y;
> int* p1 = &p->i;
> p->~Y();
> return *p1; // ???
> }
>
>
> gcc returns 0; clang, ICC and MSVC return 42. So at least one compiler
> agrees with you (the others may, too, but they aren't saying).
Again, my point is that you can't prove that "no one does X" by saying "no one
I checked has done X". It only proves attributes observable in your sample.
In this case, the sample proved that at least one implementation has done it.
On segunda-feira, 8 de agosto de 2016 22:11:35 PDT 'Edward Catmur' via ISO C++
Standard - Discussion wrote:
> Libraries can place constraints on user code; in this case the constraint
> would be on writing to the shared_ptr_state storage after
> the shared_ptr_state subobject is destroyed. The compiler can't make
> assumptions based on what user code might have done, since it doesn't know
> what the documented constraints on user code are.
True.
So I guess this boils down to a simple question: is the compiler allowed to
modify the bytes backing the storage of class types after all the destructors
of the sub-objects have run?
Note I didn't say "trivial".
Note also this should also apply to using the storage as scratch area for
other work, like running destructors of sibling sub-objects.
struct A { int i = 42; ~A() {} };int main() {alignas(A) char buf[sizeof(A)];auto* p = new (buf) A;void* p1 = &p->i;p->~A();int x;std::memcpy(&x, p1, sizeof(x));return x;}
Finally, note that there's a dual question regarding constructors: is the
compiler allowed to assume that the contents of the bytes backing the storage
of a class are unspecified before the constructor is run? Or, more specifically,
can it fill in the storage area with a pattern value?
void f(int);
struct A
{
int i;
A() { f(i); } // is the compiler allowed to assume i is unspecified?
}:
void *ptr = ::operator new(sizeof(A));
memset(ptr, 0, sizeof(A));
new (ptr) A; // it will be called from here
On segunda-feira, 8 de agosto de 2016 22:28:30 PDT 'Edward Catmur' via ISO C++
Standard - Discussion wrote:
> > exist continue to exist. They are still there. Such as in:
> > struct A
> > {
> >
> > int i;
> > ~A();
> >
> > }:
> >
> > a.~A();
> > a.i = 0; // begins new lifetime for A::i
>
> No, you can't do that; [class.cdtor]/1 says that referring to a.i is UB.
> (The examples given are the formation of pointers, but the implication is
> that forming an lvalue (which occurs first) is where the UB lies.)
Fair enough.
You would say that, provided that ptr in the example below points to valid
storage area, is suitably aligned and of sufficient size, then:
new (SomeClass) ptr;
begins the lifetime of a new object, correct? Then I should be able to do:
int *ptr = &a.i;
a.~A();
new (ptr) int();
which must begin the lifetime of a new object.
On segunda-feira, 8 de agosto de 2016 22:41:06 PDT 'Edward Catmur' via ISO C++
Standard - Discussion wrote:
> > A sub-object cannot outlive its containing object because, even if it
> > could,
> > the contents are unspecified. I submit that "contents become unspecified"
> > means
> > that the previous object's lifetime ended.
>
> I'm not familiar with that language. Is it a paraphrase of a clause in the
> Standard?
No, I'm proposing this now. It would be stronger if the lifetime of another
object had begun there.