Date: Mon, 22 Aug 2022 21:08:21 +0100
On Mon, 22 Aug 2022 at 14:36, Sébastien Bini <sebastien.bini_at_[hidden]>
wrote:
> On Mon, Aug 22, 2022 at 12:41 PM Edward Catmur <ecatmur_at_[hidden]>
> wrote:
>
>> On Mon, 22 Aug 2022 at 09:45, Sébastien Bini <sebastien.bini_at_[hidden]>
>> wrote:
>>
>>> But the default implementation does memberwise reloc-assignment. How is
>>> each subobject passed down to each reloc-assign without involving a copy at
>>> each call (and recursively down to the smallest parts)? That cannot be done
>>> without the aliasing I guess.
>>>
>>
>> A compiler-generated default implementation that is (implicitly or
>> explicitly) declared as defaulted can use aliasing, and probably should be
>> required to do so. If that isn't the case (say the relocating assignment
>> operator is defined as defaulted out-of-line in a separate TU, and LTO
>> isn't in use) then yes, without aliasing a subobject of that type would
>> need to be relocated (not copied as such) into the relocating assignment
>> operator parameter slot; and this could occur recursively.
>>
>> I would expect that this would not in practice be a problem for
>> performance, but perhaps we should mandate aliasing in this case: passing a
>> prvalue (i.e. not a glvalue, which is necessarily copied/moved to the
>> parameter slot) to a relocating assignment operator, or recursively calling
>> that relocating assignment operator from the compiler-generated relocating
>> assignment operator of a containing class.
>>
>
> I'm in favor of the default operator to be aliased. However this is not
> enough as if the assignment operator of each subclass is not aliased, then
> you may still end-up making that many copies (or relocations).
>
> I believe we do need to provide a way to clearly annotate the assignment
> operator as aliased (that reloc keyword comes in). Also, it would serve for
> assignment operators that are defaulted in the implementation file only.
>
I contend that it's enough to mandate aliasing for relocatable-by-ABI
types. Note that this doesn't affect ABI in that the callee still does the
same amount of work; the difference is that the caller is required to
observe that the source and target cannot alias and that therefore the
parameter can alias the source instead of having to be a relocated
temporary.
> What's still lacking though, but not a blocking issue in my opinion, is
> that in the assignment operator implementation, users cannot elegantly call
> a subobject's prvalue assignment operator. What if std::relocate where just
> a mere cast, similar to std::move?
>
> template <class T>
> T std::relocate(const T& d) { return static_cast<T>(d); }
>
That's spelled `auto(d)` (in C++23). But it performs decay-copy. The
signature you've written has to perform a copy, because a reference to
const cannot (logically) modify its referent.
We need a "dangerous" std::relocate, leaving the source object in destroyed
state, for use in containers: std::vector, std::optional; memory pools, etc.
Then users can write: (with: `class T : B { D _d; };` )
>
> T& T::operator=(T rhs)
> {
> B::operator=(std::relocate(rhs));
> _d = std::relocate(rhs._d);
> return *this;
> }
>
What would that do if D is non_null<unique_ptr<int>>? D cannot be copied,
and there's no suitable empty state for _d to be left in after a move, so
it must be left in a destroyed state; but it must not be double-destroyed.
If you really want to, this can be written:
T& T::operator=(T rhs) {
union { T tmp; } = { .tmp = reloc rhs }; // probably elided
B::operator=(std::relocate(static_cast<B*>(&tmp)));
_d = std::relocate(&tmp._d);
return *this; // dtors of rhs and tmp are not called
}
Yes, it's ugly, but that's because this is not good code. It's code that
should be possible to write, but only barely.
wrote:
> On Mon, Aug 22, 2022 at 12:41 PM Edward Catmur <ecatmur_at_[hidden]>
> wrote:
>
>> On Mon, 22 Aug 2022 at 09:45, Sébastien Bini <sebastien.bini_at_[hidden]>
>> wrote:
>>
>>> But the default implementation does memberwise reloc-assignment. How is
>>> each subobject passed down to each reloc-assign without involving a copy at
>>> each call (and recursively down to the smallest parts)? That cannot be done
>>> without the aliasing I guess.
>>>
>>
>> A compiler-generated default implementation that is (implicitly or
>> explicitly) declared as defaulted can use aliasing, and probably should be
>> required to do so. If that isn't the case (say the relocating assignment
>> operator is defined as defaulted out-of-line in a separate TU, and LTO
>> isn't in use) then yes, without aliasing a subobject of that type would
>> need to be relocated (not copied as such) into the relocating assignment
>> operator parameter slot; and this could occur recursively.
>>
>> I would expect that this would not in practice be a problem for
>> performance, but perhaps we should mandate aliasing in this case: passing a
>> prvalue (i.e. not a glvalue, which is necessarily copied/moved to the
>> parameter slot) to a relocating assignment operator, or recursively calling
>> that relocating assignment operator from the compiler-generated relocating
>> assignment operator of a containing class.
>>
>
> I'm in favor of the default operator to be aliased. However this is not
> enough as if the assignment operator of each subclass is not aliased, then
> you may still end-up making that many copies (or relocations).
>
> I believe we do need to provide a way to clearly annotate the assignment
> operator as aliased (that reloc keyword comes in). Also, it would serve for
> assignment operators that are defaulted in the implementation file only.
>
I contend that it's enough to mandate aliasing for relocatable-by-ABI
types. Note that this doesn't affect ABI in that the callee still does the
same amount of work; the difference is that the caller is required to
observe that the source and target cannot alias and that therefore the
parameter can alias the source instead of having to be a relocated
temporary.
> What's still lacking though, but not a blocking issue in my opinion, is
> that in the assignment operator implementation, users cannot elegantly call
> a subobject's prvalue assignment operator. What if std::relocate where just
> a mere cast, similar to std::move?
>
> template <class T>
> T std::relocate(const T& d) { return static_cast<T>(d); }
>
That's spelled `auto(d)` (in C++23). But it performs decay-copy. The
signature you've written has to perform a copy, because a reference to
const cannot (logically) modify its referent.
We need a "dangerous" std::relocate, leaving the source object in destroyed
state, for use in containers: std::vector, std::optional; memory pools, etc.
Then users can write: (with: `class T : B { D _d; };` )
>
> T& T::operator=(T rhs)
> {
> B::operator=(std::relocate(rhs));
> _d = std::relocate(rhs._d);
> return *this;
> }
>
What would that do if D is non_null<unique_ptr<int>>? D cannot be copied,
and there's no suitable empty state for _d to be left in after a move, so
it must be left in a destroyed state; but it must not be double-destroyed.
If you really want to, this can be written:
T& T::operator=(T rhs) {
union { T tmp; } = { .tmp = reloc rhs }; // probably elided
B::operator=(std::relocate(static_cast<B*>(&tmp)));
_d = std::relocate(&tmp._d);
return *this; // dtors of rhs and tmp are not called
}
Yes, it's ugly, but that's because this is not good code. It's code that
should be possible to write, but only barely.
Received on 2022-08-22 20:08:34