Date: Thu, 18 Aug 2022 17:49:44 +0100
On Thu, 18 Aug 2022 at 13:38, Sébastien Bini <sebastien.bini_at_[hidden]>
wrote:
> On Thu, Aug 18, 2022 at 12:35 AM Edward Catmur <ecatmur_at_[hidden]>
> wrote:
>
>> Ah, I'm talking about swap implemented memberwise, [...]
>>
>> Usually each data member swap would be inlined, and the compiler would
>> observe that it just needs to swap two chunks of memory, with exceptions
>> for self-referential types.
>>
>> It's certainly not more efficient than destroy-and-rebuild; and it's more
>> work (at present, you have to write out a swap function by hand). But it
>> is safe against exceptions.
>>
>
> The problem is then, as you pointed out, we don't have anything at the
> moment to default generate a memberwise swap implementation. I've seen this
> proposal P0198R0 (
> https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0198r0.pdf),
> but I don't know what kind of reception it got.
>
Ah, thanks for looking that up. I'm not sure I agree with the approach in
that paper; I'd go for a one-argument member function, maybe with a tag for
std::swap (and even, perhaps, the assignment operators) to select it. But
that's by the by.
What about a mixed approach, that could take the best of both worlds? T&
> operator=(T src) = reloc; would generate the assignment operator with the
> same aliasing as with the reloc ctor. This bit is important, as it means
> that the source object will be leaving the assignment operator in a
> destructed state (and as such its destructor will not be called, as if by
> the reloc ctor).
>
Actually, I think that aliasing on relocating assignment is not feasible.
The reason is ABI: we can currently write a (non-special) assignment
operator with the signature we want to use for relocating assignment, so
that ABI cannot change. In particular, we can declare the relocating
assignment operator in a header and then later define it out-of-line in a
source file, possibly by default: `T& T::operator=(T) = default;`. It would
be inelegant if the behavior of that assignment operator were required to
be different dependent on whether it was defaulted in-class or out-of-class.
So I think the source object must be destroyed or relocated-from (although
this can be elided).
Then I see two possible implementations:
>
> 1. If T has a noexcept reloc ctor and dtor, then destroy-and-relocate
> is performed.
> 2. Otherwise, we perform a memberwise destroy-and-relocate by calling
> the prvalue-assignment operator on each subobject:
> - The prvalue-assignment may be user-provided (in which case a
> temporary copy of the subobject is passed) or generated by =reloc.
> - This generated code shall handle exceptions in a way that is
> similar to constructor initializers: if any subobject prvalue-assignment
> operator throws, then the destructor of each source subobject that has not
> been relocated will be called before the exception is propagated. This
> should be exception safe, as if done by the copy-and-swap idiom.
> - The only problem I foresee is if T has a user-provided
> destructor, in which case the destructor body will not be called on the
> source object in case an exception is thrown. For that reason, that second
> implementation should be deleted if T has a user-provided dtor.
>
> Sure, but the user is capable of doing this themselves?
1. T& operator=(T rhs) { static_assert(noexcept(...)); this->~T(); return
*new (this) T(reloc rhs); }
2. T& operator=(T) = default; // ill-formed if ~T() is user-provided
Yes, they don't get automatic selection between the two, but I don't really
see that as an issue; it'd be quite surprising.
wrote:
> On Thu, Aug 18, 2022 at 12:35 AM Edward Catmur <ecatmur_at_[hidden]>
> wrote:
>
>> Ah, I'm talking about swap implemented memberwise, [...]
>>
>> Usually each data member swap would be inlined, and the compiler would
>> observe that it just needs to swap two chunks of memory, with exceptions
>> for self-referential types.
>>
>> It's certainly not more efficient than destroy-and-rebuild; and it's more
>> work (at present, you have to write out a swap function by hand). But it
>> is safe against exceptions.
>>
>
> The problem is then, as you pointed out, we don't have anything at the
> moment to default generate a memberwise swap implementation. I've seen this
> proposal P0198R0 (
> https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2016/p0198r0.pdf),
> but I don't know what kind of reception it got.
>
Ah, thanks for looking that up. I'm not sure I agree with the approach in
that paper; I'd go for a one-argument member function, maybe with a tag for
std::swap (and even, perhaps, the assignment operators) to select it. But
that's by the by.
What about a mixed approach, that could take the best of both worlds? T&
> operator=(T src) = reloc; would generate the assignment operator with the
> same aliasing as with the reloc ctor. This bit is important, as it means
> that the source object will be leaving the assignment operator in a
> destructed state (and as such its destructor will not be called, as if by
> the reloc ctor).
>
Actually, I think that aliasing on relocating assignment is not feasible.
The reason is ABI: we can currently write a (non-special) assignment
operator with the signature we want to use for relocating assignment, so
that ABI cannot change. In particular, we can declare the relocating
assignment operator in a header and then later define it out-of-line in a
source file, possibly by default: `T& T::operator=(T) = default;`. It would
be inelegant if the behavior of that assignment operator were required to
be different dependent on whether it was defaulted in-class or out-of-class.
So I think the source object must be destroyed or relocated-from (although
this can be elided).
Then I see two possible implementations:
>
> 1. If T has a noexcept reloc ctor and dtor, then destroy-and-relocate
> is performed.
> 2. Otherwise, we perform a memberwise destroy-and-relocate by calling
> the prvalue-assignment operator on each subobject:
> - The prvalue-assignment may be user-provided (in which case a
> temporary copy of the subobject is passed) or generated by =reloc.
> - This generated code shall handle exceptions in a way that is
> similar to constructor initializers: if any subobject prvalue-assignment
> operator throws, then the destructor of each source subobject that has not
> been relocated will be called before the exception is propagated. This
> should be exception safe, as if done by the copy-and-swap idiom.
> - The only problem I foresee is if T has a user-provided
> destructor, in which case the destructor body will not be called on the
> source object in case an exception is thrown. For that reason, that second
> implementation should be deleted if T has a user-provided dtor.
>
> Sure, but the user is capable of doing this themselves?
1. T& operator=(T rhs) { static_assert(noexcept(...)); this->~T(); return
*new (this) T(reloc rhs); }
2. T& operator=(T) = default; // ill-formed if ~T() is user-provided
Yes, they don't get automatic selection between the two, but I don't really
see that as an issue; it'd be quite surprising.
Received on 2022-08-18 16:49:57