C++ Logo

std-proposals

Advanced search

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

From: Edward Catmur <ecatmur_at_[hidden]>
Date: Wed, 17 Aug 2022 23:35:14 +0100
On Wed, 17 Aug 2022 at 17:41, S├ębastien Bini <sebastien.bini_at_[hidden]>
wrote:

> Hm, but you can write:
>>
>> T& operator=(T rhs) { std::destroy_at(this); return *new (this) T(reloc
>> rhs); }
>>
>> If we permit operator=(T), as a special member function, the same
>> aliasing power as the relocating constructor, then it invokes precisely one
>> destructor call and one relocating constructor call, which is perfect. If
>> it doesn't have aliasing on its prvalue rhs parameter (admittedly that
>> might be tricky from an ABI standpoint), then there's potentially two calls
>> to the relocating constructor, but they can be elided together.
>>
>
> Indeed, but for that to hold we do need to have a destroy-and-relocate
> implementation behind the scene right? copy-and-swap, even with the
> aliasing, costs at least two relocations + one destruction:
>

Ah, I'm talking about swap implemented memberwise, figuratively:

void T::swap(T& rhs) {
    using std::swap;
    swap(a_, rhs.a_);
    swap(b_, rhs.b_);
    swap(c_, rhs.c_);
    // ...
}

So, after inlining, `y = reloc x;` becomes:

swap(y.a_, x.a_);
swap(y.b_, x.b_);
swap(y.c_, x.c_);
// ...;
x.~T();

Usually each data member swap would be inlined, and the compiler would
observe that it just needs to swap two chunks of memory, with exceptions
for self-referential types.

It's certainly not more efficient than destroy-and-rebuild; and it's more
work (at present, you have to write out a swap function by hand). But it
is safe against exceptions.

What feels unsafe about destroy-and-relocate is that we destruct an object
>>>> within a member function. We can tweak things around and turn the
>>>> reloc-assignment operator into a static function behind the scene:
>>>> T& operator=(T src) = reloc; // same as: static T& assign_reloc(T& dst,
>>>> T& src) { std::destroy_at(&dst); return *std::relocate_at(&src, &dst); }
>>>>
>>>> Here we no longer have the scary std::destroy_at(this), and we avoid
>>>> self-destruction in a member function (which may be an UB?).
>>>>
>>>
>>> That's still destroying an object from within its member function, just
>>> not directly. It's fine, though; `delete this` has always been legal, it's
>>> just never been a sign of good code. One library I use regularly
>>> (rapidjson) uses destroy-and-rebuild for (lvalue) assignment, and it works
>>> fine - as long as you sort out exception safety.
>>>
>>
> How am I destroying from a member function?
>

operator=(T) is a member function, even if it's a defaulted special member
function.

Is the reloc-assignment such a blocking point for this proposal? Can't we
> leave it out at first, knowing that users can still easily write their own
> (albeit it won't be defaulted nor aliased)? Can't a second proposal solve
> this specific point once we have more user experience as you mentioned?
>

We need defaultable relocating assignment for aggregate classes which have
data members that are relocate-assignable but not copy-assignable or
move-assignable. So at minimum for rule-of-zero classes relocating
assignment should be defaulted by default to memberwise relocating
assignment. Then, if users wish to explicitly declare relocating
assignment as defaulted on such classes they should be able to do so.

Perhaps the MVP is to default relocating assignment as deleted for classes
with a user-defined destructor; that is, if the user has declared a
user-defined destructor there is no way for them to access the
compiler-generated (memberwise) relocating assignment, but they must write
one by hand (either destroy-and-rebuild, copy-and-swap, or (ugh) something
bespoke).

Received on 2022-08-17 22:35:27