> But you need std::relocate, AFAIK, in order to implement an "emplace_back-by-relocating" operation.

What is proposed is:
std::vector<T, Alloc>::push_back(std::relocate_t, T value)
{
    /* relocate value using std::relocate_at at the end of the vector */
}
std::relocate_t being a tag type to distinguish between other push_back overloads. It is then called like that:

std::vector<T> vec;
T obj;
vec.push_back(std::relocate, reloc obj); // with inline constexpr std::relocate_t std::relocate{};

That way the object is relocated into the function parameter and then from the function parameter to the vector. There may be ways to alleviate that extra relocation.

It is on one hand clearer to write, and on the other hand has the advantage of allowing you to relocate local variables.

> Unless there's a really good reason to have two distinct yet synonymous syntaxes, I strongly recommend picking one and sticking with it. (And I recommend the standard, placement-new, syntax, because you certainly can't get rid of it from the language.)

That's just syntactic sugar, but do I find `reloc (&to) from` much clearer than `new (&to) T{reloc from}`. Besides it emphasizes that no temporary is created. Anyway, that's just a detail for the moment.

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

Yes, I am telling you the rules as of today. The focus wasn't put on reloc assignment as you pointed out. I do believe there is less need to "reloc-assign" than to "reloc-construct", as they solve different problems.

> I agree with your aesthetic sense there: rewriting `a = b` into "destroy a, construct a from b" doesn't seem desirable. But you do need to tackle this problem somehow.

The more I think of it, the more "destroy a, construct a from b" seems to be the best thing to do for relocatable, non-movable types. We then need to see how to allow for it for those types.

> Hm. Could you write out the rules that you're imagining for
>     operator reloc(T&&) = default;
>     operator reloc(T&&) = delete;

Basically we will follow the rules that are already in play for the other constructors. In what follows, I use the term subobject to mention a direct base class or a non-static data member.
For a class type T:
  • operator reloc(T&&) is implicitly declared if T has no user-declared copy constructor, move constructor and destructor.
  • The default generated (explicitly defaulted or via implicit definition) operator reloc(T&&):
    • performs memberwise relocation:
      • either via memcpy if the subobject is trivially relocatable,
      • via the subobject's own operator reloc(),
      • via synthesized relocation (move+destroy) if the subobject does not have an accessible operator reloc().
    • may merely delegate to the move constructor + destructor (full synthesized relocation) if none of its subobjects is trivially relocatable or has an accessible operator reloc() (i.e. synthesized relocation would be used for all subobjects).
  • An operator reloc() that is implicitly declared or defaulted can be deleted if its default definition would be ill-formed (for instance one of its subobjects has a deleted operator reloc() and an inaccessible move constructor or destructor).
In addition, and I guess that bits should interest you, the class type T is trivially relocatable:
  • if it T is trivial or is an array of trivial types, or
  • if T provides an accessible defaulted (explicitly defaulted or via implicit definition) operator reloc() and all of its subobjects are recursively trivially relocatable.
Trivial relocatability is the trickiest part and I probably got something wrong somewhere.

Best regards,
Sébastien


On Tue, May 31, 2022 at 3:31 PM Arthur O'Dwyer <arthur.j.odwyer@gmail.com> wrote:
On Tue, May 31, 2022 at 4:51 AM 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:

You're right that swap can be written in terms of std::relocate_at, not std::relocate.
But you need std::relocate, AFAIK, in order to implement an "emplace_back-by-relocating" operation. See
https://quuxplusone.github.io/blog/2022/05/18/std-relocate/
and in particular its emplace_back example on Godbolt:
https://godbolt.org/z/cqPP4oeE9
I don't think this example is possible unless you have access to something like std::relocate that can produce prvalues.
 
BTW 'new (&rhs) T(reloc temp);' can be replaced by placement-reloc: 'reloc (&rhs) temp;'

Unless there's a really good reason to have two distinct yet synonymous syntaxes, I strongly recommend picking one and sticking with it. (And I recommend the standard, placement-new, syntax, because you certainly can't get rid of it from the language.)


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

We could go further and claim that assigning from a reloc statement transparently calls destructor + placement reloc (std::destroy_at(&q1); reloc (&q1) p2;). I am not sure that's desirable at all

I agree with your aesthetic sense there: rewriting `a = b` into "destroy a, construct a from b" doesn't seem desirable. But you do need to tackle this problem somehow.

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

–Arthur