On Wed, 17 Aug 2022 at 17:41, Sébastien Bini <sebastien.bini@gmail.com> wrote:
Hm, but you can write:

T& operator=(T rhs) { std::destroy_at(this); return *new (this) T(reloc rhs); }

If we permit operator=(T), as a special member function, the same aliasing power as the relocating constructor, then it invokes precisely one destructor call and one relocating constructor call, which is perfect.  If it doesn't have aliasing on its prvalue rhs parameter (admittedly that might be tricky from an ABI standpoint), then there's potentially two calls to the relocating constructor, but they can be elided together.

Indeed, but for that to hold we do need to have a destroy-and-relocate implementation behind the scene right? copy-and-swap, even with the aliasing, costs at least two relocations + one destruction:

Ah, I'm talking about swap implemented memberwise, figuratively:

void T::swap(T& rhs) {
    using std::swap;
    swap(a_, rhs.a_);
    swap(b_, rhs.b_);
    swap(c_, rhs.c_);
    // ...
}

So, after inlining, `y = reloc x;` becomes:

swap(y.a_, x.a_);
swap(y.b_, x.b_);
swap(y.c_, x.c_);
// ...;
x.~T();

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.

What feels unsafe about destroy-and-relocate is that we destruct an object within a member function. We can tweak things around and turn the reloc-assignment operator into a static function behind the scene:
T& operator=(T src) = reloc; // same as: static T& assign_reloc(T& dst, T& src) { std::destroy_at(&dst); return *std::relocate_at(&src, &dst); }

Here we no longer have the scary std::destroy_at(this), and we avoid self-destruction in a member function (which may be an UB?).

That's still destroying an object from within its member function, just not directly.  It's fine, though; `delete this` has always been legal, it's just never been a sign of good code.  One library I use regularly (rapidjson) uses destroy-and-rebuild for (lvalue) assignment, and it works fine - as long as you sort out exception safety.

How am I destroying from a member function?

operator=(T) is a member function, even if it's a defaulted special member function.

Is the reloc-assignment such a blocking point for this proposal? Can't we leave it out at first, knowing that users can still easily write their own (albeit it won't be defaulted nor aliased)? Can't a second proposal solve this specific point once we have more user experience as you mentioned?

We need defaultable relocating assignment for aggregate classes which have data members that are relocate-assignable but not copy-assignable or move-assignable.  So at minimum for rule-of-zero classes relocating assignment should be defaulted by default to memberwise relocating assignment.  Then, if users wish to explicitly declare relocating assignment as defaulted on such classes they should be able to do so.

Perhaps the MVP is to default relocating assignment as deleted for classes with a user-defined destructor; that is, if the user has declared a user-defined destructor there is no way for them to access the compiler-generated (memberwise) relocating assignment, but they must write one by hand (either destroy-and-rebuild, copy-and-swap, or (ugh) something bespoke).