C++ Logo

std-proposals

Advanced search

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

From: Edward Catmur <ecatmur_at_[hidden]>
Date: Fri, 6 May 2022 02:37:42 +0100
On Thu, 5 May 2022 at 21:48, Arthur O'Dwyer <arthur.j.odwyer_at_[hidden]>
wrote:

> And how do you relocate an object inside a container? Do you need an
>> std::vector<T>::push_back(std::optional<T>) API? I'd find that disturbing.
>>
>
> You can in fact use the regular `emplace_back`, via a pattern I call the
> "super constructing super elider":
> https://quuxplusone.github.io/blog/2018/05/17/super-elider-round-2/
> However, it's still pretty disturbing. :)
>
> Here's a test case using P1144r6 syntax with the magic library function `T
> std::relocate(T*)`:
>
> void relocate_into_a_vector(std::vector<Widget>& v, Widget *pw) {
> struct S {
> Widget *pw_;
> operator Widget() const { return std::relocate(pw_); }
> };
> v.emplace_back(S{pw});
> }
>

Hm, and what if reserve() throws? You need to end the lifetime (i.e.,
destroy) the source object in that case, so you need a bool to say whether
relocation has occurred. (Or, for strong exception safety, you may want to
do something else - but you still need to known whether the lifetime of the
source object has ended.) So a std::relocate_wrapper<T> will be necessary,
since you won't want users writing it themselves.


> Disagreed, and here's why.
> [...]
>
> Analogously, your `operator reloc` should permit relocators to throw. The
> relocator has two jobs: to *construct* the destination object and to
> *destroy* the source object. Both of these jobs agree about what should
> happen if an exception is thrown: C++ should take care of cleaning up the
> already-constructed and/or not-yet-destroyed parts of the destination
> and/or source object. If relocation fails, then constructing-the-new-object
> failed (so there is no new object) and also destroying-the-old-object
> "failed" (so there is no old object either). *A failed relocation
> operation means that neither object exists anymore.*
>

I see a relocator as having a single purpose, which is to perform the *inverse
of prvalue materialization*: to convert an lvalue to a prvalue. That
prvalue will subsequently be materialized into another lvalue, but that's
not what relocation is *for*. Absolutely, when relocate of a complete
object is implemented as move-plus-destroy, it is performing two tasks, and
should either of those throw it's safe for both objects to be destroyed
according to how far the move-plus-destroy has proceeded. And obviously
trivial relocators can't throw. The problem is user-defined relocation.

In practical terms, what happens if we have a type T with user-defined
destructor, and to release some resource ~T accesses members x and y, with
x having throwing (move-plus-destroy) relocate? Suppose x's destructor
throws after dst.x has been move-constructed from src.x; then there is no
object of T (with members x and y both within their lifetime) that we can
call ~T on, so the resource will leak. This is unacceptable, so the only
safe thing to do if an exception is thrown midway through a user-defined
relocate is to terminate.

The slightly odd thing here is that it means it's only absolutely necessary
for a type's relocator to be noexcept (or at least default to
noexcept(true)) if it has a user-defined destructor. For consistency I feel
that all relocators (that is, relocators proper, not move+destroy
combinations) should default to noexcept, but I'm open to arguments
otherwise.

I guess this is good motivation for me to finally publish a P1144r6.
> P1144r5 gave the wrong exception behavior to `std::uninitialized_relocate`
> (i.e. P1144r5 *disagreed* with my indisputable logic above, because I had
> not yet figured it out). David Stone convinced me to change it, more than a
> year ago. But I never got around to submitting P1144r6 to a mailing. It's
> just been sitting around in draft format since March 2021:
>
> https://quuxplusone.github.io/draft/d1144-object-relocation.html#wording-uninit-relocate
>

Ah, great. I'll definitely make the time to read it in depth.

P1144r6 proposes a magic library utility function `std::relocate(T*)`:
>
> https://quuxplusone.github.io/draft/d1144-object-relocation.html#wording-relocate
> With this magic function, you could express the above as
> {
> std::string src1 = "hello world";
> std::string dst1 = std::relocate(src1);
> std::string src2 = "hello world";
> std::string *dstp = ~~~~;
> ::new ((void*)dstp) std::string(std::relocate(src2));
> ~~~~
> } // Here dst1.~string() is implicitly called, as are src1.~string()
> and src2.~string().
> // But src1 and src2 were already destroyed, so that's a double-free
> bug and Bad Stuff happens.
> // P1144 doesn't try to solve this part; it just claims you shouldn't
> be doing that.
>

Yes, and I've said that std::optional should hide the manual
lifetime-management away so you don't get double-free. And that,
accordingly, std::relocate should have a considerably uglier name to
dissuade casual usage.

But also I'm becoming more convinced that a reloc operator (applicable to
automatic variables, with straightforward behavior) should be viable, will
be useful and should alleviate a lot of confusion around move semantics.
Relocation could well require language change anyway, so this is an
opportunity to make it useful for everyone.

Received on 2022-05-06 01:37:54