On Wed, 18 May 2022 at 05:56, Sébastien Bini <sebastien.bini@gmail.com> wrote:
> The wrinkle here is that for a relocator synthesized from move+destroy, the
> destination and source lifetime do overlap, observably

I am honestly confused here: what kind of synthesized relocation are you talking about? The reloc operator does not perform move+destruct. If the object is not trivially relocatable, and does not have an accessible operator reloc() member function, then all that reloc does is constructing a new object using the move (or copy) constructor, no destruction is involved by reloc per-se:

I'm talking about relocation as performed memberwise on base-and-members under a relocation operation. In general, I prefer to regard relocation as a single operation that can be accomplished in various different ways, and that delayed destruction can be part of the operation; this is more consistent than saying that operator reloc ends the lifetime of some objects but not others.

A class that provides any user-defined copy constructor, move constructor, or destructor will have its operator reloc() implicitly deleted. Class authors are still free to default it or provide a definition.

Yes, this is fine.
 
If a class has any of its base class or data-member with an implicitly deleted operator reloc(), then its operator reloc() will be implicitly deleted as well. And writing a custom operator reloc() may prove to be complicated. Consider:

// B has an implicitly deleted operator reloc(), as it provides for instance a user-defined move constructor and destructor.
struct T : B
{
    D data;

    // User defined operator reloc()?
    operator reloc(T&& src) :
        B{std::move(src)}
        /* data is initialized using default rules */
    { src.~B(); /* manually destruct B part */ }
}


My take is that if T's operator reloc() were defaulted, then it would get deleted instead of having a default definition, as B's operator reloc() is implicitly deleted.

Oof, no. We do not want anyone to feel like they should manually destruct subobjects. Acceptable ways to write this are:

    operator reloc(T&& src) :
        B{std::move(src)}
        /* data is initialized using default rules */
    {} // src::B is automatically destroyed

If a base-or-member has an initializer, then the corresponding base-or-member on the source object is destroyed automatically - I'm not sure exactly when, it could be immediately, or before entering the function body, or after completing the function body.

But also, T should benefit from D's relocation operator, even if B is only relocatable by move-and-destroy. So if T does not declare a relocation operator, relocating T should be accomplished by move+destroy on its B base subobject and relocate proper on it's D data member.

This is the same mechanism that applies to move constructors. AFAIK default-generated move-constructors do not delegate to their base class copy constructor if their base class move constructor is implicitly deleted. I am not sure it is a good idea to have operator reloc() make a synthesized relocation, calling move+destruct if the base class or data-member has an implicitly deleted operator reloc() member function, if that is what you are suggesting.

Yes, that's exactly what I'm suggesting. Composition is necessary - here this means being able to combine a relocate-only and a move-destroy type as data members of a class, and have that aggregate remain usable.

What problems do you foresee?
 
> Any class that is trivially relocatable or has an accessible relocate
> operator must either have been trivial already (in which case there is no
> ABI impact) or have been explicitly opted in to relocation, by having a
> relocator declared or defaulted. Since this is under the control of the
> library author and/or user, I don't see it as an issue to say that
> functions with parameters having trivial or user-defined relocate have a
> different ABI (callee-destroy, or if necessary having automatic
> accompanying flags in registers or on the stack). We accept that adding a
> user-defined destructor, or virtual functions/bases, or changing data
> members changes ABI, so why shouldn't defaulting/declaring a relocator?

That's interesting, but I see some obstacles:

operator reloc() is not necessarily opt-in. It may be implicitly declared and defined. This requires that the class has no user-defined constructors and destructors (among other things). A class may get an operator reloc() for free, without any changes in the class declaration nor in any of its base classes and data-members (recursively). Functions that will take such classes as a prvalue parameter will then have an ABI break?
Under those circumstances the ABI break may not be necessary. Indeed such classes are POD types, and their destructor are a no-op. I guess we don't really care that their destructor call cannot be alleviated.

POD is a deprecated term; presumably you mean trivial? It is not observable if a trivial class is relocated by move+destroy, so there is no ABI break for these.
 
For the other cases, operator reloc() is opt-in. However it is vital that most standard library classes support this operator reloc(). Most STL classes provide a user-defined constructor or destructor, and as such will get an implicitly deleted operator reloc(). If they don't default or provide their own operator reloc(), then any class that has an STL data-member will get its operator reloc() implicitly deleted as well. They will then struggle writing their own operator reloc() to support relocation, and will in addition be ruled out of trivial relocation (double pain).

I don't believe this is necessary. Whether the relocate operator is suppressed should depend primarily on the class itself (on whether its other special member functions are user-defined), not on its constituent types. It is only only if a base or member is not relocatable by either relocation proper or by fallback to move+destroy that the relocation operator becomes non-defaultable (also if the class has a user-defined destructor and some base or member has throwing relocate) (it may still be possible to write a user-defined relocator, if a member has alternate constructors, say).
 
If the STL then explicitly supports operator reloc() (like it should be required) then any function having an STL parameter passed by value (like void foo(std::unique_ptr<T>); ) will get an ABI break. I really have no idea if that is acceptable.

Thiago has made an important and (to me) novel point regarding this; I'll reply later.
 
> We have a precedent for noexcept(true)-by-default and that's the destructor. I
> think it could be acceptable to make the relocator obey the same rule because
> after it's run -- and whether it has thrown or not -- one object's lifetime
> has ended.

Given that, AFAIK, there is no synthesized relocation involved, then yes the relocator (operator reloc() member function) can be noexcept(true) by default. Then any exception that leaks through a relocator causes std::terminate to be called.
 
I don't see enough benefit to noexcept(true) by default to justify the potential loss of credibility.