C++ Logo

std-proposals

Advanced search

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

From: Sébastien Bini <sebastien.bini_at_[hidden]>
Date: Fri, 19 Aug 2022 17:26:50 +0200
On Fri, Aug 19, 2022 at 2:29 PM Edward Catmur <ecatmur_at_[hidden]>
wrote:

> That being said, now lies the problem on how to write a custom
>> implementation for this assignment operator. For the relocation ctor, we
>> worked around this by saying that omitted subobject initializers would
>> automatically call the appropriate reloc ctor of said subobject. We cannot
>> do that magic for the reloc-assign, as there is no initializer list.
>> However, I believe that std::relocate can come to the rescue here:
>>
>> class T : B
>> {
>> D _d;
>> reloc T& operator=(T rhs)
>> { // exception safety is omitted for the example
>> B::operator=(std::relocate(static_cast<B*>(&rhs)));
>> _d = std::relocate(&rhs._d);
>> return *this;
>> }
>> };
>>
>> std::relocate is a nice fit here as, thanks to the aliasing, we know
>> that rhs dtor will not be called. In the same manner, std::relocate
>> could also be used to initialize subobjects in the reloc ctor.
>>
>
> Sorry, no. Aliasing doesn't prevent the destructor being called, it just
> means that the parameter is the same object as (has the same address as)
> the argument. It still has to be (will be) destroyed, either by the
> destructor or by the relocating constructor. If you want to write code
> like that, you need to relocate rhs into a buffer (placement-new-relocate)
> or union.
>

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.

As we consider that the reloc constructor destroys its parameter, the
aliased-prvalue-assignment does the same. And that's why we need aliasing.

This provides consistency between the relocation constructor, and
relocation assignment operator (aka aliased-prvalue-assignment operator).
Likewise, the fact that the relocation constructor leaves the source object
in a destroyed state allows us to use std::relocate on each source's
subobject in ctor implementation (so we can manually forward to
data-member's relocation ctor). The aliased-prvalue-assignment operator
would allow the same use of std::relocate, so users can manually forward to
data-member's prvalue-assignment operator (aliased or not). This would be
similar to how we use std::move/std::exchange both in move constructor and
move assignment operator.

In my opinion the aliased-prvalue-assignment is simple to write and keep
things consistent with the relocation constructor.

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?
   - in its implementation, how do you forward to subobject
   prvalue-assignment operator, without involving recursively each time a copy
   of said subobject?
   - the consistency with the relocation constructor?
   - 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.



> As a side note, I wonder to what extent std::relocate could not be a
>> special function that, as reloc, simply turns some address into a prvalue,
>> without actually performing any relocation. That could allow nice
>> optimization bits in user-defined reloc ctor or assignment operator.
>>
>
> Definitely, if that can be made to work. That's really nice; it supports
> my idea of relocation being conceptually a change in value category.
>

Yes, but for the moment, I have no idea how to pull that one off :/ I need
to give it more thoughts.


> I feel mixed with having = default; doing only memberwise assignments:
>>
>> - This will force all users that have a user-provided dtor to write
>> their own reloc-assignment operator, while it is likely that their reloc
>> ctor and dtor are noexcept :/
>> - If = default also generates destroy-and-relocate then it enables
>> nice recursion. Without this some classes will need to write their own
>> reloc-assignment operator while the default generated destroy-and-relocate
>> would fit like a glove. Take unique_ptr for instance. This one has a
>> legitimate user-provided dtor, and as such is not eligible to
>> memberwise-reloc-assign (which is good, as it would otherwise leak its own
>> resource). As a consequence std::unique_ptr would need to provide a
>> reloc-assign definition, which will ultimately, just free its resource and
>> steal that of the source object. Something that the default generated
>> destroy-and-relocate would do for free.
>>
>> My point is that, if = default (a) generates destroy-and-relocate, and
>> (b) falls back to memberwise-reloc-assignments, then users will almost
>> never need to write any implementation. Think of case (a) as the
>> base/halting cases of any recursive function.
>>
>
> Against this, there's the inconsistency with the other special assignment
> operators (copy and move). Consistency is important for teachability and
> to ensure that users have the correct mental model. Unless you're
> proposing to change those as well...
>
> I feel that it's OK to expect library authors (a small proportion of users
> in general) to write `T& operator=(T rhs) noexcept { this->~T(); return
> *new (this) T(reloc rhs); }`. It's only 30 tokens, against 10 to explicitly
> default the relocating assignment operator.
>

Okay, sold in favor of consistency w/t the other operators, which I have no
intent to change :)

Received on 2022-08-19 15:27:03