On Thu, Aug 18, 2022 at 6:49 PM Edward Catmur <ecatmur@googlemail.com> wrote:
Actually, I think that aliasing on relocating assignment is not feasible. The reason is ABI: we can currently write a (non-special) assignment operator with the signature we want to use for relocating assignment, so that ABI cannot change.  In particular, we can declare the relocating assignment operator in a header and then later define it out-of-line in a source file, possibly by default: `T& T::operator=(T) = default;`. It would be inelegant if the behavior of that assignment operator were required to be different dependent on whether it was defaulted in-class or out-of-class.

Yes, for the aliasing to work, the = reloc part must appear in the declaration. I admit this is not ideal, and we should split the aliasing part from the definition.

As such, we could write:

    reloc T& operator=(T); // = default; or user-defined in implementation file

"reloc" here would turn on the aliasing magic. It would live in the same space as "virtual" and "static", and would not be part of the function signature. It needs not to be rewritten in the function definition. "reloc" could only be added on the prvalue-assignment operator, and users would not be allowed to take the address of such a function (as it is a special function with a special ABI).

And as sugar, we could add that if the prvalue-assignment operator is defaulted in its declaration then the reloc aliasing is implicit. This works as today there is no default code generation for the prvalue-assignment operator.

    // in class declaration:
    T& operator=(T) = default: // reloc is implicitly added

That being said, now lies the problem on how to write a custom implementation for this assignment operator. For the relocation ctor, we worked around this by saying that omitted subobject initializers would automatically call the appropriate reloc ctor of said subobject. We cannot do that magic for the reloc-assign, as there is no initializer list. However, I believe that std::relocate can come to the rescue here:

    class T : B
    {
        D _d;
        reloc T& operator=(T rhs)
        { // exception safety is omitted for the example
            B::operator=(std::relocate(static_cast<B*>(&rhs)));
            _d = std::relocate(&rhs._d);
            return *this;
        }
    };

std::relocate is a nice fit here as, thanks to the aliasing, we know that rhs dtor will not be called. In the same manner, std::relocate could also be used to initialize subobjects in the reloc ctor.

As a side note, I wonder to what extent std::relocate could not be a special function that, as reloc, simply turns some address into a prvalue, without actually performing any relocation. That could allow nice optimization bits in user-defined reloc ctor or assignment operator.

Sure, but the user is capable of doing this themselves?

1. T& operator=(T rhs) { static_assert(noexcept(...)); this->~T(); return *new (this) T(reloc rhs); }
2. T& operator=(T) = default; // ill-formed if ~T() is user-provided

Yes, they don't get automatic selection between the two, but I don't really see that as an issue; it'd be quite surprising.

I feel mixed with having = default; doing only memberwise assignments:
  • This will force all users that have a user-provided dtor to write their own reloc-assignment operator, while it is likely that their reloc ctor and dtor are noexcept :/
  • If = default also generates destroy-and-relocate then it enables nice recursion. Without this some classes will need to write their own reloc-assignment operator while the default generated destroy-and-relocate would fit like a glove. Take unique_ptr for instance. This one has a legitimate user-provided dtor, and as such is not eligible to memberwise-reloc-assign (which is good, as it would otherwise leak its own resource). As a consequence std::unique_ptr would need to provide a reloc-assign definition, which will ultimately, just free its resource and steal that of the source object. Something that the default generated destroy-and-relocate would do for free.
My point is that, if = default (a) generates destroy-and-relocate, and (b) falls back to memberwise-reloc-assignments, then users will almost never need to write any implementation. Think of case (a) as the base/halting cases of any recursive function.

Regards,
Sébastien