C++ Logo

std-proposals

Advanced search

Re: [std-proposals] Relocation in C++

From: Edward Catmur <ecatmur_at_[hidden]>
Date: Mon, 22 Aug 2022 11:41:00 +0100
On Mon, 22 Aug 2022 at 09:45, Sébastien Bini <sebastien.bini_at_[hidden]>
wrote:

>
>>>> - in its implementation, how do you forward to subobject
>>>> prvalue-assignment operator, without involving recursively each time a copy
>>>> of said subobject?
>>>>
>>>> Preferably, you don't; you use destroy-and-rebuild, or copy-and-swap
>>> (where the copy is a potentially-elided relocate). If you really want to,
>>> you relocate into a buffer or union, taking your own responsibility for
>>> relocating or destroying each subobject.
>>>
>>
> 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.

An aside; for types that have the new callee-destroy ABI (through being
>> relocate-only, or through declaring a relocating constructor and not opting
>> out of the new argument passing ABI), the copy part of copy-and-swap is
>> unnecessary: swap simpliciter is sufficient, since the source object is
>> then destroyed at the closing brace of the relocating assignment operator.
>> But copy-and-swap is manifestly safer for use with caller-destroy types.
>>
>
> I don't get that point. The copy is either elided or done by relocation
> right (given that we have a reloc ctor)?
>

Yes, but even relocation is unnecessary; a reloc-and-swap implementation of
T::operator=(T rhs) is written as:

T::operator=(T rhs) {
    T tmp(reloc rhs); // calls T::T(T), relocating rhs into tmp and
obviating destructor call on rhs
    tmp.swap(*this); // hand-written to swap subobjects memberwise
} // tmp.~T(); is called at end of scope

But given that T is callee-destroy (which it must be for the reloc part of
reloc-and-swap to actually invoke the relocating constructor as opposed to
the move constructor), this could just be:

T::operator=(T rhs) {
    rhs.swap(*this); // hand-written to swap subobjects memberwise
} // rhs.~T(); is called at end of scope (and never delayed till end of
caller-expression)

I'm split about this reloc-assignment.
>
> - If we don't have aliasing then we have a problem with the default
> implementation. = default does memberwise reloc-assign, and I don't know
> how it is going to perform. Surely making recursive copies of all
> subjobjects down to their smallest bits is not acceptable.
> - If we have aliasing then we are fine with the default
> implementation. But then we need to decide whether it is the responsibility
> of the aliased-prvalue-assignment to destroy the source object.
> - If yes, then as you pointed down, some uncautious developer may
> easily write leaks.
> - If not then the aliased-prvalue-assignment cannot alleviate the
> call to the destructor of the source, even by relocating from it (we are no
> better than move-assign)... Indeed, if the aliased-prvalue-assignment could
> optionally destroy the source object (by relocating from it for instance),
> then the caller-site would need to know whether it still needs to call the
> destructor on the source. The ABI does not allow to propagate that kind of
> information (from callee to caller). (Sure, we could change the ABI for
> this operator only (which we are already doing with aliasing), but let's
> not push this too far.) We don't have that kind of issue with the
> relocation constructor as it is in charge of destroying the source object,
> and as such the caller-site needs not to take any further action.
> - Consequently, if we have aliasing and that
> aliased-prvalue-assignment is not in charge of destroying the source
> object, then we shoot ourselves in the foot, as then we cannot even
> implement destroy-and-relocate, which would seem like a pretty obvious
> implementation for such an operator... Likewise we would not fully support
> relocation-only types.
>
> I'd very much prefer to have aliasing and the destruction of the source
> object to be the responsibility of the operator. That would be opt-in with
> the reloc keywork in the operator declaration, in place of virtual or
> static. It would feel very consistent with how the reloc ctor works, and
> the default implementation will not be deoptimized. I still don't know how
> to make it safe to write though :/
>

The ABI would be the same as that for passing any other type by value that
is non-trivially relocatable and has not opted out of ABI break; that is,
it would be the responsibility of the relocating assignment operator to
destroy its RHS operand. The only difference would be that when called as
x = reloc y; elision of the relocating constructor on y would be mandatory
(y would not need to be relocated into rhs of T::operator=(T rhs), since
rhs would alias y; this is safe wrt. self-assignment since if x aliases y
the code already has UB). The call site then knows that y has been
destroyed and does not emit a further destructor call.

However, what I think you're missing is that the destructor call would
still be emitted automatically within the relocating assignment operator,
unless obviated either by the assignment operator being defaulted (in which
case there is no code for the user to get wrong) or by relocating from the
parameter. If the user really does want to take responsibility for
destroying the RHS operand, they would reloc into a union member (which
hopefully would be elided).

Received on 2022-08-22 10:41:13