On Mon, 22 Aug 2022 at 09:45, Sébastien Bini <sebastien.bini@gmail.com> wrote:
  • in its implementation, how do you forward to subobject prvalue-assignment operator, without involving recursively each time a copy of said subobject?
Preferably, you don't; you use destroy-and-rebuild, or copy-and-swap (where the copy is a potentially-elided relocate).  If you really want to, you relocate into a buffer or union, taking your own responsibility for relocating or destroying each subobject.

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.

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)

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).