On Tue, 31 May 2022 at 02:51, Sébastien Bini <sebastien.bini@gmail.com> wrote:
Actually I am not sure there are any real benefits of having `T std::relocate(T*)` if we have the reloc operator (which is a safer version of std::relocate) and std::relocate_at. swap can be written without std::relocate:

template<class T> requires is_noexcept_relocatable_v<T>
void swap(T& lhs, T& rhs)
{
    std::aligned_storage_t<sizeof(T), alignof(T)> tmp;
    std::relocate_at(&lhs, reinterpret_cast<T*>(&tmp));
    std::relocate_at(&rhs, &lhs);
    std::relocate_at(reinterpret_cast<T*>(&tmp), &rhs);
}

Nit: std::aligned_storage is deprecated, we just use alignas(T) std::byte buf[sizeof(T)].

More importantly, I don't think this would be constexpr at present, because of the reinterpret_cast. Either relocate_at should have overloads taking void* with T specified explicitly, or this will need to be written using union:

template<class T> requires is_noexcept_relocatable_v<T>
constexpr void swap(T& lhs, T& rhs) {
    union { char c; T t; } u = { .c = '\0' };
    relocate_at(&lhs, &u.t);
    relocate_at(&rhs, &lhs);
    relocate_at(&u.t, &rhs);
    u.c = '\0';
}

And I'm not 100% sure that this is constexpr (&u.t is doing a lot of work). So I'd prefer the version using std::relocate, std::relocate_at, operator reloc. This is a good demonstration of why all 3 of std::relocate, std::relocate_at, operator reloc are necessary.

On Tue, 31 May 2022 at 07:31, Arthur O'Dwyer <arthur.j.odwyer@gmail.com> wrote:
> gsl::non_null<int*> p1, p2, p3 = ...;
> gsl::non_null<int*> q1 = reloc p1;  // OK in your world
> q1 = reloc p2;  // OK??
> std::swap(q1, p3);  // OK??

`reloc obj` (with obj of type T) merely returns a temporary object of type T, built by relocating from obj (just like std::relocate). obj is considered destructed and as such is not left in a moved-from state. We rely on mechanisms such as copy-elision or clearly identified patterns (object initialisation with a reloc statement, like: `auto a = reloc obj;`) to remove that temporary in most cases.

With `q1 = reloc p2;`, as it is phrased in the paper, reloc p2 will return a temporary that is assigned into q1. p2 is considered destructed (relocated into the temporary). Then q1's move assignment operator will be called.

q1 has no "move assignment operator", remember?  We're postulating that gsl::non_null<int*> is one of these types that lacks a moved-from state. So it certainly cannot have a move assignment operator nor a move constructor.
 
Oops, my bad. The key insight is that gsl::non_null can have a *by-value* assignment operator:

non_null& operator=(non_null rhs) {
    (reloc rhs).swap(*this);
    return *this;
}

[...]
> Similarly, if you "relocated" a `std::mutex`, bad things would happen.  You can't have a mutex suddenly change memory addresses and expect the rest of the program to just be cool with that.

Yes there are objects that are not even relocatable, like std::mutex. Such types can prohibit relocation by deleting their operator reloc: `operator reloc(T&&) = delete;` (which should already be implicitly deleted since their move constructor is also deleted.)

Hm. Could you write out the rules that you're imagining for
    operator reloc(T&&) = default;
    operator reloc(T&&) = delete;
as well as what happens by default?
(IIUC, you're imagining that `operator reloc` would work the same as `operator<=>` — an explicitly =defaulted definition gets a memberwise implementation, but if you don't say anything then the operator is absent, not defaulted.)
It sounds like you're imagining simultaneously that
    struct S1 {
        M m;
        S1(S1&&);
        operator reloc(S1&&) = default;
    };
will have a defaulted (memberwise) `operator reloc`, and that
    struct S2 {
        M m;
        S2(S2&&) = delete;
    };
will have a deleted `operator reloc` "since their move constructor is also deleted" (i.e., S2's operator reloc won't just be absent; it'll be deleted).

My take:

* operator reloc(T&&) = default: 
Each direct base and member is relocated, in declaration order. Relocation is by invoking operator reloc if present, otherwise by move-and-destroy (the "relocation operation"); destructors (for move-and-destroy subobjects) are not interleaved but are invoked at the end, in reverse declaration order. Trivial if each direct base and member is trivially relocatable; that is, if the selected relocation operation is trivial. Deleted if the relocation operation for any direct base or member is inaccessible or deleted, or if the class has declared destructor (not declared as defaulted or deleted) and any direct base or member has noexcept(false) relocation operation. noexcept(true) if each direct base and member is nothrow relocatable, otherwise noexcept(false), in which case cleanup destroys subobjects of the source object in reverse declaration order (whose lifetime has not already ended), followed by subobjects of the destination object in reverse declaration order (whose lifetime has begun); the class destructor is not called.

* operator reloc(T&&) = delete;
The class is not relocatable: keyword reloc, std::relocate etc. will fail; type_traits will report false; and a deriving or composing class will have deleted operator reloc, even if the class has accessible non-deleted move constructor and destructor. (In that case, it can be made (non-trivially) relocatable by wrapping in a class whose operator reloc explicitly invokes move-and destroy, i.e.: struct U : T { operator reloc(U&& rhs) : T(std::move(rhs)) {} };.)

* If not declared:
Rule of Zero: if no Ro5 SMF (dtor, copy/move ctor, copy/move assign) is declared (including declared as defaulted or deleted), then implicitly present as if declared as defaulted. Otherwise, absent; the class is still relocatable (possibly trivially relocatable) if move ctor and dtor are accessible and not deleted.