On Mon, Aug 22, 2022 at 10:08 PM Edward Catmur <ecatmur@googlemail.com> wrote:
On Mon, 22 Aug 2022 at 14:36, Sébastien Bini <sebastien.bini@gmail.com> wrote:
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.

I contend that it's enough to mandate aliasing for relocatable-by-ABI types. Note that this doesn't affect ABI in that the callee still does the same amount of work; the difference is that the caller is required to observe that the source and target cannot alias and that therefore the parameter can alias the source instead of having to be a relocated temporary.

If I understand correctly, you suggest that aliasing be mandated only for types that have a relocation constructor and have not opted-out of the ABI break? I like this idea, it further avoids another keyword in the operator declaration.

But what about trivial types, which follow the rule of zero?

struct S { int _d; S& operator=(S rhs) { _d = rhs._d; return *this; }  };

They will get a relocation constructor for free, even if none of their subobjects explicitly provides a relocation constructor. Had they declared a prvalue-assignment operator, then it would change its ABI silently. It's okay for them to silently go from caller-destroy to callee-destroy as their destructor is a no-op, but aliasing will also force the parameter to share the same address as the caller's site source object, which is a silent ABI break.

They could still opt-out with the class attribute, but that requires manual inspection of the code from users.
 
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); }

That's spelled `auto(d)` (in C++23).  But it performs decay-copy.  The signature you've written has to perform a copy, because a reference to const cannot (logically) modify its referent.

Nice, I didn't know of that upcoming auto(d) feature.
 
We need a "dangerous" std::relocate, leaving the source object in destroyed state, for use in containers: std::vector, std::optional; memory pools, etc.

See below.
 
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;
}

What would that do if D is non_null<unique_ptr<int>>? D cannot be copied, and there's no suitable empty state for _d to be left in after a move, so it must be left in a destroyed state; but it must not be double-destroyed.

Nice find... Indeed, for the relocation to happen the caller must be in the position of preventing the destructor call on the source object. With an std::relocate just promoting to a prvalue is not enough to trigger relocation, as you pointed out. Even for types that are only-relocatable, such an std::relocate could not guarantee relocation.

That also means that, in the prvalue-assignment operator, users cannot manually forward to a subobject prvalue-assignment operator if that subobject is relocation only. (Well, unless we use the union trick or otherwise).

And yes, we do need an unsafe destructive relocation function then.