C++ Logo

std-proposals

Advanced search

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

From: Sébastien Bini <sebastien.bini_at_[hidden]>
Date: Mon, 22 Aug 2022 10:45:03 +0200
On Sat, Aug 20, 2022 at 1:13 AM Edward Catmur <ecatmur_at_[hidden]>
wrote:

> On Fri, 19 Aug 2022 at 18:15, Edward Catmur <ecatmur_at_[hidden]>
> wrote:
>
>> On Fri, 19 Aug 2022 at 16:27, Sébastien Bini <sebastien.bini_at_[hidden]>
>> wrote:
>>
>>> That's not how I see it. I agree with the address part, but I don't see
>>> why the destructor of the source object would still need to be called.
>>>
>>> In `auto x = reloc y;` given that `y` has not opted-out of the ABI
>>> break, it is being relocated into `x`, and its destructor will not be
>>> called. We consider that the relocation constructor has done the job
>>> already.
>>>
>>> Why would it be different for the assignment operator? In `x = reloc
>>> y;`, with the aliased-prvalue-assignment operator, `y` is also being
>>> relocated into `x`. The caller site can perfectly alleviate the call to `y`
>>> destructor, as it would do if the relocation constructor were called.
>>> Likewise, we consider that the aliased-prvalue-assignment operator has done
>>> the job already.
>>>
>>
>> Yes, but we can't trust the user to correctly destroy or relocate-from
>> each subobject.
>>
>> With a relocating constructor, as you'll remember, the default behavior
>> is to memberwise relocate each subobject; for each subobject mentioned in
>> the base-and-member initialization list, to initialize that subobject on
>> the target object according to its initializer and then to destruct the
>> corresponding subobject on the source object, but for each subobject not so
>> mentioned, to perform (destructive) relocation. By that means, we ensure
>> that each subobject of the source object is either destroyed or
>> (destructively) relocated-from.
>>
>> With a relocating assignment operator, there is no analogous syntax, so
>> no way to ensure that each subobject is in fact destroyed. Suppose you add
>> a data member std::unique_ptr<int> T::_p but forget to relocate it in your
>> relocating assignment operator; that would be a leak. This is too much of
>> a footgun to be acceptable.
>>
>
That's a valid concern... Still I'd very much like this operator to work
like that, but I still can't figure out a way to make it safe.

Of course, a defaulted relocating assignment operator doesn't have this
>> issue. But that's all-or-nothing.
>>
>> As we consider that the reloc constructor destroys its parameter, the
>>> aliased-prvalue-assignment does the same. And that's why we need aliasing.
>>>
>>> [...]
>>>
>>> I get what you mean, the prvalue-assignment operator could simply be not
>>> aliased and take its input parameter as a copy. But I fail to see:
>>>
>>> - where the relocation is involved, except in the potentially elided
>>> copy? If relocation is not involved any further, why even mention
>>> prvalue-assignment in the proposal?
>>>
>>> It's not a copy; it's a relocated instance, with that relocation
>> potentially elided. If you then use destroy-and-rebuild, the sequence is
>> then:
>>
>> T::T(T); // relocate source into assignment-operator parameter
>> T::~T(); // destroy target
>> T::T(T); // relocate parameter into target
>>
>> And with elision, it's:
>>
>> T::~T(); // destroy target
>> T::T(T); // relocate source into target
>>
>> Note that the destructor of the source object is not called, nor of the
>> parameter (if elision does not occur); this is because both have been
>> relocated-from, thus obviating the destructor call.
>>
>
Thank you for the clarification.

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

>
>>> - the consistency with the relocation constructor?
>>>
>>> By = default; or by destroy-and-rebuild.
>>
>>>
>>> - and the biggest flaw IMO: what would be the actual gain compared
>>> to the move assignment operator? What's great with the relocation
>>> constructor, or with the aliased-prvalue-assignment, is that they are in
>>> charge of the destruction of the source object. They can leave the source
>>> object in a dirty or inconsistent state, that would be broken if passed to
>>> a destructor, because then it just becomes unused uninitialized memory. If
>>> the prvalue-assignment operator is not in charge of the destruction of its
>>> source object, it fails to hold one of the promises of relocation and we
>>> are no better than with the move assignment operator.
>>>
>>> Oh! Burying the lede there. It'd be great if we didn't have to call the
>> destructor, but we have to be consistent with existing code. And there are
>> at least two strategies available that work with non-movable objects (like
>> non_null<unique_ptr>): destroy-and-relocate, and relocate-and-swap. I'd
>> like to give this some more thought, though.
>>
>
> To expand on this a little further:
>
> * the gain relative to the move assignment operator is that for types that
> are non-movable by virtue of not having an empty state, the only way to
> successfully write a move assignment operator is for it to swap the
> contents of source and target. But since the source of such an assignment
> operator is an xvalue, not a prvalue, its destruction may be delayed
> indefinitely, leading to potential hazards e.g. deadlocks. However, for a
> relocating assignment operator, we can ensure that the source object is
> destroyed within the body of the relocating assignment operator (by
> relocating from it), so we can release the resource previously owned by the
> target within the body of the relocating assignment operator, either before
> (destroy-and-rebuild) or immediately after (copy-and-swap) we transfer the
> resource owned by the source object to the target object.
>
* consistency with existing code: one can already write a non-special
> assignment operator with the same signature as we intend to use for
> relocating assignment. We don't want such code to change meaning and
> possibly develop a leak.
>
* there are three ways to avoid the destructor call on the source object:
> relocate into the target (having previously destroyed it); relocate into a
> temporary (and then swap the contents of that temporary with the target);
> relocate into a union member (and then take responsibility for resources).
> Adding another method is unnecessary.
>

Indeed, all valid points.


> 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)?

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 :/

Received on 2022-08-22 08:45:16