Hi all,

> That is, the destructor is called immediately, since the reloc operator ended the scope of the name. I think that's a good thing.

I cannot be called immediately if the move ctor is called, as it could then lead to objects being destructed twice:

void foo(T objA);
void fwd_to_foo(T objB) { foo(reloc objB); }

If reloc destructs objB then the caller of fwd_to_foo cannot know that it was destructed (assuming there is no ABI change).

> 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.

Agreed.

> 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.)

Isn't it unusual to force different requirements (here on the reloc operator) depending on whether the object to relocate is a function parameter?
Also, If you cannot relocate function parameters, how would you relocate an object inside an std::optional? You could still use 'emplace' but this more or less forces you to wrap all relocatable types into an optional...

And how do you relocate an object inside a container? Do you need an std::vector<T>::push_back(std::optional<T>) API? I'd find that disturbing.

As much as I would love to, I don't see how we can conciliate relocation-only types with existing ABIs...

> 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.

Agreed.

> > 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.

rhs would be considered as destructed when control exits 'operator reloc' function body. So its destructor is not called.

> 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.

I feel mixed about this idea. Developers may find it weird not to have a reference somewhere. Besides I don't know how we could write base-or-member initializers that would call their operator reloc member function (as if by =default) if they all take parameters as value, not as reference. This would imply that a copy is made somewhere, unless some new language rule applies.

> 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.

Again, I am not sure how this would work if parameters are captured by value (which would imply a copy is made somewhere).

> 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.

That would mean the function body cannot use the source parameter as it is considered as destructed? It would be the first time a function is not allowed to use its input parameter. But I agree it would feel safer as the source object is logically destructed at this point (even though most of the time it would be untouched).

> 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)

We do need a new syntax to call this special relocation constructor. The main reason is for std::relocate_at implementation (which is needed for container implementations). std::relocate_at(const T* src, T* dst) either (a) makes memcpy if trivially relocatable, (b) constructs dst with some kind of placement-new with the relocation ctor/dtor, or (c) placement-new move ctor + dtor: new (dst) T{const_cast<T&&>(*src)}; src->~T();

In the first two revisions of my proposal, (b) was implemented by a call to the relocation ctor directly: new (dst) T{const_cast<T~>(*src)}; That worked well because of the relocation reference.

The syntax I suggested in my last email (T::T reloc(T&&)) allowed to write the placement new: new (dst) T reloc{const_cast<T&&>(*src)};

If we find a good relocation constructor / operator reloc call syntax, then we should get the base-or-member-initializer syntax for free.

Best regards,
Sébastien

On Wed, May 4, 2022 at 2:54 PM Edward Catmur <ecatmur@googlemail.com> wrote:
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.