On Sat, Aug 20, 2022 at 1:13 AM Edward Catmur <ecatmur@googlemail.com> wrote:
On Fri, 19 Aug 2022 at 18:15, Edward Catmur <ecatmur@googlemail.com> wrote:
On Fri, 19 Aug 2022 at 16:27, Sébastien Bini <sebastien.bini@gmail.com> wrote:
That's not how I see it. I agree with the address part, but I don't see why the destructor of the source object would still need to be called.

In `auto x = reloc y;` given that `y` has not opted-out of the ABI break, it is being relocated into `x`, and its destructor will not be called. We consider that the relocation constructor has done the job already.

Why would it be different for the assignment operator? In `x = reloc y;`, with the aliased-prvalue-assignment operator, `y` is also being relocated into `x`. The caller site can perfectly alleviate the call to `y` destructor, as it would do if the relocation constructor were called. Likewise, we consider that the aliased-prvalue-assignment operator has done the job already.

Yes, but we can't trust the user to correctly destroy or relocate-from each subobject.

With a relocating constructor, as you'll remember, the default behavior is to memberwise relocate each subobject; for each subobject mentioned in the base-and-member initialization list, to initialize that subobject on the target object according to its initializer and then to destruct the corresponding subobject on the source object, but for each subobject not so mentioned, to perform (destructive) relocation.  By that means, we ensure that each subobject of the source object is either destroyed or (destructively) relocated-from.

With a relocating assignment operator, there is no analogous syntax, so no way to ensure that each subobject is in fact destroyed.  Suppose you add a data member std::unique_ptr<int> T::_p but forget to relocate it in your relocating assignment operator; that would be a leak.  This is too much of a footgun to be acceptable.
 
That's a valid concern... Still I'd very much like this operator to work like that, but I still can't figure out a way to make it safe.

Of course, a defaulted relocating assignment operator doesn't have this issue. But that's all-or-nothing.

As we consider that the reloc constructor destroys its parameter, the aliased-prvalue-assignment does the same. And that's why we need aliasing.

[...]

I get what you mean, the prvalue-assignment operator could simply be not aliased and take its input parameter as a copy. But I fail to see:
  • where the relocation is involved, except in the potentially elided copy? If relocation is not involved any further, why even mention prvalue-assignment in the proposal?
It's not a copy; it's a relocated instance, with that relocation potentially elided.  If you then use destroy-and-rebuild, the sequence is then:

T::T(T); // relocate source into assignment-operator parameter
T::~T(); // destroy target
T::T(T); // relocate parameter into target

And with elision, it's:

T::~T(); // destroy target
T::T(T); // relocate source into target

Note that the destructor of the source object is not called, nor of the parameter (if elision does not occur); this is because both have been relocated-from, thus obviating the destructor call.

Thank you for the clarification.
  • 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.
  • the consistency with the relocation constructor?
By = default; or by destroy-and-rebuild. 
  • and the biggest flaw IMO: what would be the actual gain compared to the move assignment operator? What's great with the relocation constructor, or with the aliased-prvalue-assignment, is that they are in charge of the destruction of the source object. They can leave the source object in a dirty or inconsistent state, that would be broken if passed to a destructor, because then it just becomes unused uninitialized memory. If the prvalue-assignment operator is not in charge of the destruction of its source object, it fails to hold one of the promises of relocation and we are no better than with the move assignment operator.
Oh! Burying the lede there.  It'd be great if we didn't have to call the destructor, but we have to be consistent with existing code. And there are at least two strategies available that work with non-movable objects (like non_null<unique_ptr>): destroy-and-relocate, and relocate-and-swap. I'd like to give this some more thought, though.

To expand on this a little further:

* the gain relative to the move assignment operator is that for types that are non-movable by virtue of not having an empty state, the only way to successfully write a move assignment operator is for it to swap the contents of source and target. But since the source of such an assignment operator is an xvalue, not a prvalue, its destruction may be delayed indefinitely, leading to potential hazards e.g. deadlocks. However, for a relocating assignment operator, we can ensure that the source object is destroyed within the body of the relocating assignment operator (by relocating from it), so we can release the resource previously owned by the target within the body of the relocating assignment operator, either before (destroy-and-rebuild) or immediately after (copy-and-swap) we transfer the resource owned by the source object to the target object.
* consistency with existing code: one can already write a non-special assignment operator with the same signature as we intend to use for relocating assignment. We don't want such code to change meaning and possibly develop a leak.
* there are three ways to avoid the destructor call on the source object: relocate into the target (having previously destroyed it); relocate into a temporary (and then swap the contents of that temporary with the target); relocate into a union member (and then take responsibility for resources). Adding another method is unnecessary.

Indeed, all valid points.
 
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)?

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 :/