Date: Tue, 31 May 2022 18:06:04 +0200
> 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_at_[hidden]>
wrote:
> On Tue, May 31, 2022 at 4:51 AM Sébastien Bini <sebastien.bini_at_[hidden]>
> 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
>
>>
"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_at_[hidden]>
wrote:
> On Tue, May 31, 2022 at 4:51 AM Sébastien Bini <sebastien.bini_at_[hidden]>
> 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
>
>>
Received on 2022-05-31 16:06:17