Date: Mon, 22 Aug 2022 15:36:15 +0200
On Mon, Aug 22, 2022 at 12:41 PM Edward Catmur <ecatmur_at_[hidden]>
wrote:
> On Mon, 22 Aug 2022 at 09:45, Sébastien Bini <sebastien.bini_at_[hidden]>
> wrote:
>
>> But the default implementation does memberwise reloc-assignment. How is
>> each subobject passed down to each reloc-assign without involving a copy at
>> each call (and recursively down to the smallest parts)? That cannot be done
>> without the aliasing I guess.
>>
>
> A compiler-generated default implementation that is (implicitly or
> explicitly) declared as defaulted can use aliasing, and probably should be
> required to do so. If that isn't the case (say the relocating assignment
> operator is defined as defaulted out-of-line in a separate TU, and LTO
> isn't in use) then yes, without aliasing a subobject of that type would
> need to be relocated (not copied as such) into the relocating assignment
> operator parameter slot; and this could occur recursively.
>
> I would expect that this would not in practice be a problem for
> performance, but perhaps we should mandate aliasing in this case: passing a
> prvalue (i.e. not a glvalue, which is necessarily copied/moved to the
> parameter slot) to a relocating assignment operator, or recursively calling
> that relocating assignment operator from the compiler-generated relocating
> assignment operator of a containing class.
>
I'm in favor of the default operator to be aliased. However this is not
enough as if the assignment operator of each subclass is not aliased, then
you may still end-up making that many copies (or relocations).
I believe we do need to provide a way to clearly annotate the assignment
operator as aliased (that reloc keyword comes in). Also, it would serve for
assignment operators that are defaulted in the implementation file only.
> An aside; for types that have the new callee-destroy ABI (through being
>>> relocate-only, or through declaring a relocating constructor and not opting
>>> out of the new argument passing ABI), the copy part of copy-and-swap is
>>> unnecessary: swap simpliciter is sufficient, since the source object is
>>> then destroyed at the closing brace of the relocating assignment operator.
>>> But copy-and-swap is manifestly safer for use with caller-destroy types.
>>>
>>
>> I don't get that point. The copy is either elided or done by relocation
>> right (given that we have a reloc ctor)?
>>
>
> Yes, but even relocation is unnecessary; a reloc-and-swap implementation
> of T::operator=(T rhs) is written as:
>
> T::operator=(T rhs) {
> T tmp(reloc rhs); // calls T::T(T), relocating rhs into tmp and
> obviating destructor call on rhs
> tmp.swap(*this); // hand-written to swap subobjects memberwise
> } // tmp.~T(); is called at end of scope
>
> But given that T is callee-destroy (which it must be for the reloc part of
> reloc-and-swap to actually invoke the relocating constructor as opposed to
> the move constructor), this could just be:
>
> T::operator=(T rhs) {
> rhs.swap(*this); // hand-written to swap subobjects memberwise
> } // rhs.~T(); is called at end of scope (and never delayed till end of
> caller-expression)
>
Indeed, thanks for clarifying.
> I'm split about this reloc-assignment.
>>
>> - If we don't have aliasing then we have a problem with the default
>> implementation. = default does memberwise reloc-assign, and I don't know
>> how it is going to perform. Surely making recursive copies of all
>> subjobjects down to their smallest bits is not acceptable.
>> - If we have aliasing then we are fine with the default
>> implementation. But then we need to decide whether it is the responsibility
>> of the aliased-prvalue-assignment to destroy the source object.
>> - If yes, then as you pointed down, some uncautious developer may
>> easily write leaks.
>> - If not then the aliased-prvalue-assignment cannot alleviate the
>> call to the destructor of the source, even by relocating from it (we are no
>> better than move-assign)... Indeed, if the aliased-prvalue-assignment could
>> optionally destroy the source object (by relocating from it for instance),
>> then the caller-site would need to know whether it still needs to call the
>> destructor on the source. The ABI does not allow to propagate that kind of
>> information (from callee to caller). (Sure, we could change the ABI for
>> this operator only (which we are already doing with aliasing), but let's
>> not push this too far.) We don't have that kind of issue with the
>> relocation constructor as it is in charge of destroying the source object,
>> and as such the caller-site needs not to take any further action.
>> - Consequently, if we have aliasing and that
>> aliased-prvalue-assignment is not in charge of destroying the source
>> object, then we shoot ourselves in the foot, as then we cannot even
>> implement destroy-and-relocate, which would seem like a pretty obvious
>> implementation for such an operator... Likewise we would not fully support
>> relocation-only types.
>>
>> I'd very much prefer to have aliasing and the destruction of the source
>> object to be the responsibility of the operator. That would be opt-in with
>> the reloc keywork in the operator declaration, in place of virtual or
>> static. It would feel very consistent with how the reloc ctor works, and
>> the default implementation will not be deoptimized. I still don't know how
>> to make it safe to write though :/
>>
>
> The ABI would be the same as that for passing any other type by value that
> is non-trivially relocatable and has not opted out of ABI break; that is,
> it would be the responsibility of the relocating assignment operator to
> destroy its RHS operand. The only difference would be that when called as
> x = reloc y; elision of the relocating constructor on y would be mandatory
> (y would not need to be relocated into rhs of T::operator=(T rhs), since
> rhs would alias y; this is safe wrt. self-assignment since if x aliases y
> the code already has UB). The call site then knows that y has been
> destroyed and does not emit a further destructor call.
>
> However, what I think you're missing is that the destructor call would
> still be emitted automatically within the relocating assignment operator,
> unless obviated either by the assignment operator being defaulted (in which
> case there is no code for the user to get wrong) or by relocating from the
> parameter. If the user really does want to take responsibility for
> destroying the RHS operand, they would reloc into a union member (which
> hopefully would be elided).
>
Indeed, you are right. Unlike in the relocation constructor, where the
destructor of the source object is not called (unless synthesized
relocation with a delegating constructor happens), it will indeed be called
in the assignment operator (unless reloc is used on RHS).
This part should make everything work then:
- the default implementation should work fine with aliasing,
- the assignment operator is still in charge of destructing the source
object, and we can write destroy-and-relocate or swap implementations.
What's still lacking though, but not a blocking issue in my opinion, is
that in the assignment operator implementation, users cannot elegantly call
a subobject's prvalue assignment operator. What if std::relocate where just
a mere cast, similar to std::move?
template <class T>
T std::relocate(const T& d) { return static_cast<T>(d); }
Then users can write: (with: `class T : B { D _d; };` )
T& T::operator=(T rhs)
{
B::operator=(std::relocate(rhs));
_d = std::relocate(rhs._d);
return *this;
}
std::relocate is safe to use as it no longer leaves the source object in a
destroyed state. Temporaries are still created for B and _d subobjects
(even if their assignment operator is aliased), but it still allows for
nice, exception-safe code. The new overload resolution rules will pick the
prvalue-assignment operator if it exists, or else move or copy assignment
operator.
And we can still write std::relocate_at in terms of std::relocate:
template <class T>
T* std::relocate_at(const T* src, T* dst) { new (dst)
T{std::relocate(*src)}; } // selected only T has a relocation ctor.
// we cannot use std::construct_at as it does not have a prvalue overload,
and as such std::relocate_at is not constexpr :/
My only concern is then users will confuse reloc and std::relocate. We
mentioned a while ago to rename std::relocate due its dangerous nature, but
if it's a mere cast then that point does not hold anymore.
wrote:
> On Mon, 22 Aug 2022 at 09:45, Sébastien Bini <sebastien.bini_at_[hidden]>
> wrote:
>
>> But the default implementation does memberwise reloc-assignment. How is
>> each subobject passed down to each reloc-assign without involving a copy at
>> each call (and recursively down to the smallest parts)? That cannot be done
>> without the aliasing I guess.
>>
>
> A compiler-generated default implementation that is (implicitly or
> explicitly) declared as defaulted can use aliasing, and probably should be
> required to do so. If that isn't the case (say the relocating assignment
> operator is defined as defaulted out-of-line in a separate TU, and LTO
> isn't in use) then yes, without aliasing a subobject of that type would
> need to be relocated (not copied as such) into the relocating assignment
> operator parameter slot; and this could occur recursively.
>
> I would expect that this would not in practice be a problem for
> performance, but perhaps we should mandate aliasing in this case: passing a
> prvalue (i.e. not a glvalue, which is necessarily copied/moved to the
> parameter slot) to a relocating assignment operator, or recursively calling
> that relocating assignment operator from the compiler-generated relocating
> assignment operator of a containing class.
>
I'm in favor of the default operator to be aliased. However this is not
enough as if the assignment operator of each subclass is not aliased, then
you may still end-up making that many copies (or relocations).
I believe we do need to provide a way to clearly annotate the assignment
operator as aliased (that reloc keyword comes in). Also, it would serve for
assignment operators that are defaulted in the implementation file only.
> An aside; for types that have the new callee-destroy ABI (through being
>>> relocate-only, or through declaring a relocating constructor and not opting
>>> out of the new argument passing ABI), the copy part of copy-and-swap is
>>> unnecessary: swap simpliciter is sufficient, since the source object is
>>> then destroyed at the closing brace of the relocating assignment operator.
>>> But copy-and-swap is manifestly safer for use with caller-destroy types.
>>>
>>
>> I don't get that point. The copy is either elided or done by relocation
>> right (given that we have a reloc ctor)?
>>
>
> Yes, but even relocation is unnecessary; a reloc-and-swap implementation
> of T::operator=(T rhs) is written as:
>
> T::operator=(T rhs) {
> T tmp(reloc rhs); // calls T::T(T), relocating rhs into tmp and
> obviating destructor call on rhs
> tmp.swap(*this); // hand-written to swap subobjects memberwise
> } // tmp.~T(); is called at end of scope
>
> But given that T is callee-destroy (which it must be for the reloc part of
> reloc-and-swap to actually invoke the relocating constructor as opposed to
> the move constructor), this could just be:
>
> T::operator=(T rhs) {
> rhs.swap(*this); // hand-written to swap subobjects memberwise
> } // rhs.~T(); is called at end of scope (and never delayed till end of
> caller-expression)
>
Indeed, thanks for clarifying.
> I'm split about this reloc-assignment.
>>
>> - If we don't have aliasing then we have a problem with the default
>> implementation. = default does memberwise reloc-assign, and I don't know
>> how it is going to perform. Surely making recursive copies of all
>> subjobjects down to their smallest bits is not acceptable.
>> - If we have aliasing then we are fine with the default
>> implementation. But then we need to decide whether it is the responsibility
>> of the aliased-prvalue-assignment to destroy the source object.
>> - If yes, then as you pointed down, some uncautious developer may
>> easily write leaks.
>> - If not then the aliased-prvalue-assignment cannot alleviate the
>> call to the destructor of the source, even by relocating from it (we are no
>> better than move-assign)... Indeed, if the aliased-prvalue-assignment could
>> optionally destroy the source object (by relocating from it for instance),
>> then the caller-site would need to know whether it still needs to call the
>> destructor on the source. The ABI does not allow to propagate that kind of
>> information (from callee to caller). (Sure, we could change the ABI for
>> this operator only (which we are already doing with aliasing), but let's
>> not push this too far.) We don't have that kind of issue with the
>> relocation constructor as it is in charge of destroying the source object,
>> and as such the caller-site needs not to take any further action.
>> - Consequently, if we have aliasing and that
>> aliased-prvalue-assignment is not in charge of destroying the source
>> object, then we shoot ourselves in the foot, as then we cannot even
>> implement destroy-and-relocate, which would seem like a pretty obvious
>> implementation for such an operator... Likewise we would not fully support
>> relocation-only types.
>>
>> I'd very much prefer to have aliasing and the destruction of the source
>> object to be the responsibility of the operator. That would be opt-in with
>> the reloc keywork in the operator declaration, in place of virtual or
>> static. It would feel very consistent with how the reloc ctor works, and
>> the default implementation will not be deoptimized. I still don't know how
>> to make it safe to write though :/
>>
>
> The ABI would be the same as that for passing any other type by value that
> is non-trivially relocatable and has not opted out of ABI break; that is,
> it would be the responsibility of the relocating assignment operator to
> destroy its RHS operand. The only difference would be that when called as
> x = reloc y; elision of the relocating constructor on y would be mandatory
> (y would not need to be relocated into rhs of T::operator=(T rhs), since
> rhs would alias y; this is safe wrt. self-assignment since if x aliases y
> the code already has UB). The call site then knows that y has been
> destroyed and does not emit a further destructor call.
>
> However, what I think you're missing is that the destructor call would
> still be emitted automatically within the relocating assignment operator,
> unless obviated either by the assignment operator being defaulted (in which
> case there is no code for the user to get wrong) or by relocating from the
> parameter. If the user really does want to take responsibility for
> destroying the RHS operand, they would reloc into a union member (which
> hopefully would be elided).
>
Indeed, you are right. Unlike in the relocation constructor, where the
destructor of the source object is not called (unless synthesized
relocation with a delegating constructor happens), it will indeed be called
in the assignment operator (unless reloc is used on RHS).
This part should make everything work then:
- the default implementation should work fine with aliasing,
- the assignment operator is still in charge of destructing the source
object, and we can write destroy-and-relocate or swap implementations.
What's still lacking though, but not a blocking issue in my opinion, is
that in the assignment operator implementation, users cannot elegantly call
a subobject's prvalue assignment operator. What if std::relocate where just
a mere cast, similar to std::move?
template <class T>
T std::relocate(const T& d) { return static_cast<T>(d); }
Then users can write: (with: `class T : B { D _d; };` )
T& T::operator=(T rhs)
{
B::operator=(std::relocate(rhs));
_d = std::relocate(rhs._d);
return *this;
}
std::relocate is safe to use as it no longer leaves the source object in a
destroyed state. Temporaries are still created for B and _d subobjects
(even if their assignment operator is aliased), but it still allows for
nice, exception-safe code. The new overload resolution rules will pick the
prvalue-assignment operator if it exists, or else move or copy assignment
operator.
And we can still write std::relocate_at in terms of std::relocate:
template <class T>
T* std::relocate_at(const T* src, T* dst) { new (dst)
T{std::relocate(*src)}; } // selected only T has a relocation ctor.
// we cannot use std::construct_at as it does not have a prvalue overload,
and as such std::relocate_at is not constexpr :/
My only concern is then users will confuse reloc and std::relocate. We
mentioned a while ago to rename std::relocate due its dangerous nature, but
if it's a mere cast then that point does not hold anymore.
Received on 2022-08-22 13:36:29