On Tue, 3 May 2022 at 10:42, Sébastien Bini <sebastien.bini@gmail.com> wrote:
Hello all,

I've been digesting yesterday discussions, and here is what I can propose to move forward:
  • types cannot be relocated if they don't have a copy constructor or a move constructor defined and accessible.
 I strongly believe that relocate-only types should be supported; it is acceptable that they would not work as function parameters (the user can wrap them in std::optional if necessary).
  • we remove relocation references and the relocation constructor.
  • we introduce a new "operator reloc(T&& rhs)" static member function in classes, which will act both as a constructor and a destructor (much like P0023 relocator). This function can be defaulted, implicitly declared and defined just like constructors. This would likely require new syntax bits, more on that later.
  • we keep the reloc operator. Calling reloc on an object will "relocate" the instance and mark the end of scope of its name. How the object is "relocated" is left to compiler vendors. The language offers the following relocation methods:
    •   If the type is trivially relocatable (p1144), then using std::memcpy is authorized. If this method is used then the language enforces that the destructor of the relocated object is not called.
    •   If the type provides an accessible "operator reloc" method, then it can be used to perform the relocation. Again the language enforces that the destructor of the relocated object is not called.
    •   Simply use a regular move (std::move). The destructor of the moved instance is called normally (i.e. when it goes out of scope).
The only case where the destructor needs to be called is function arguments under caller-destroy ABI, so it is only necessary for the behavior to be implementation-defined (i.e., possibly invoking move + delayed destroy) in the case of function arguments. For automatic variables it should be consistent to immediately relocate (trivially or user-defined) with no destructor call, or copy/move + immediate destroy if no better relocation operation is available.

Then, the copy/move constructor only needs to be accessible if a function argument is relocated, not if the user is careful to only relocate automatic variables. (It should be accessible even if the ABI is callee-destroy, so that the correctness of code is not platform-dependent.)

I propose this function to have this signature "operator reloc(T&&) [noexcept]". noexcept shall be optional but throwing from such a function shall be an UB (just like it is an UB to throw from a destructor IIRC). 

This is incorrect; it is allowed to throw from a destructor that is marked noexcept(false). std::terminate is called (not UB, this is perfectly well defined) if a noexcept(true) destructor (the default since C++11) throws, or if an exception escapes a destructor invoked for stack unwind during exception handling. So you can even throw from a destructor during stack unwinding, as long as someone catches it before it hits the unwinder.

I agree that the relocator should be unconditionally noexcept, such that it should not be possible to throw out from it (but std::terminate, not UB). This is because the source object will have been partially relocated and thus will be incapable of releasing resources.

I suggest the following initializer syntax for operator reloc:

    struct T : B {
        M data;
       
        // user-implementation equivalent to =default;
        operator reloc(T&& rhs) noexcept : B reloc{std::move(rhs)}, data reloc{std::move(rhs.data)} {}
    };

Would this call the destructor on the argument? In that case, it wouldn't work for relocate-only bases and data members.

Also, it isn't clear from this syntax that rhs is going to be immediately destroyed (or at least, for caller-destroy function arguments, that accessing it between now and its eventual destructor call is forbidden); since this is a totally new interface, you may as well take the argument by value. Then also there is no way for user code to call operator reloc in a dangerous manner.

@Edward Catmur: I read your suggestion about having automatic member initialisation as if defaulted, but I think it would contrast too much from constructors. Besides, users may have specific initialisation needs? We could still come up with something like 'operator reloc(T&& rhs) noexcept = bitcopies' (P1029) and still provide a function body to adjust self references...

 `= bitcopies` would be incorrect if a data member is not trivially relocatable; we already have `= default` to indicate that a special member function performs memberwise defaults and will be trivial if possible (i.e. if memberwise trivial), so this should also work here.  Then, `= default` performing memberwise relocate indicates that should be the default if a base or member initializer is omitted. Thus there's no need for new syntax to indicate that a particular base-or-member is relocated. So your example becomes simply:

operator reloc(T rhs) {} // equivalent to = default (other than not being trivial)

If a base-or-member-initializer is provided, presumably that initializer would be OK to access the corresponding and later subobjects on the source. The corresponding subobject would not be relocated (unless you can suggest syntax that would work for bases) but instead would be destructed immediately after that base-or-member-initializer completes. Then by the time the function body is entered the source object has been completely destroyed (some bases or data members relocated, others specifically destroyed) and its lifetime ended.