C++ Logo

std-proposals

Advanced search

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

From: Magnus Fromreide <magfr_at_[hidden]>
Date: Tue, 3 May 2022 09:07:36 +0200
Hello!

This is probably unrelated but what happens with sliced objects?

Consider

struct Base { };

void f(Base~ x) { }

struct Derived : Base { };

void g() {
  Derived d;
  f(reloc d);
}

Who is responsible for the destruction of the parts added in Derived
when it is relocated to a Base?

/MF

On Mon, May 02, 2022 at 06:16:41PM +0200, Marcin Jaczewski via Std-Proposals wrote:
> pon., 2 maj 2022 o 17:35 Sébastien Bini via Std-Proposals
> <std-proposals_at_[hidden]> napisał(a):
> >
> > Hello,
> >
> > > > In fwd_to_sink, reloc cannot omit the call to the destructor of 'y', as it is called by 'foo'. For this to work:
> > > > - fwd_to_sink would need to somehow return some information on the destruction state of its parameters.
> > > > - or change the call convention so that the parameters are destructed in the called function body instead of the callee site.
> > >
> > > Yes, I recognize the issue. This can be resolved by stating that implementations may refuse to relocate function parameters, and providing a mechanism (e.g. an attribute) for the author to switch ABI to callee-destroy.
> >
> > It's more complicated than that. If relocation also destructs objects, then you allow for a new category of unmovable objects to support relocation. For example, std::lock_guard could be relocatable with no change.
> > Then how do you deal with this?
> >
> > void foo(lock_guard<mutex> lg);
> > void fwd_to_foo(lock_guard<mutex> lg) { foo(reloc lg); }
> > void bar(mutex& m)
> > {
> > std::lock_guard lg{m};
> > fwd_to_foo(reloc lg);
> > }
> >
> > This code will work fine if reloc avoids the destructor call. But it will not if "implementations refuse to relocate", as lock_guard is not movable. We cannot have code optionally compile depending on which ABI we pick, can we?
> >
> > Best regards,
> > Sébastien
> >
> Yes, the order of destructors is the critical thing, but if we are
> explicit then we could change it.
> Your syntax `T~` in parameters could mean the same as `T` but "callee
> is responsible for destruction".
> This could be meant as "redirect of life time".
>
> Current problem with move is that the caller does not have info about
> what happens with `T` in callee and needs to call destructor after
> function call,
> but if we push responsibility to callee that knows it we can skip all
> code in the caller as this variable should be already destroyed.
>
> Even if we have two diffrent variables like in case of:
>
> ```
> T foo(T~ b)
> {
> return reloc b;
> }
> T bar(T~ a)
> {
> return foo(reloc a); //init `b` and call `a` destructor BEFORE
> call to `foo`, we can locally verify that nobody touch `a` memory
> after that
> }
> ```
>
> This could even allow tail-call-recursion that is impossible for
> current movable types.
>
> And this nicely fit with constructors:
>
> ```
> struct T
> {
> T(T~ a)
> {
> //
> }
> };
> ```
> this is a normal constructor but the value passed there will be
> destroyed by this constructor not the caller, same logic as in the
> ordinal function with `~`.
> And this allows many optimizations like simply skipping the destructor
> as the compiler can prove that it is noop in this function that is
> impossible to do in the general case.
>
>
>
>
>
> > On Mon, May 2, 2022 at 3:54 PM Edward Catmur <ecatmur_at_[hidden]> wrote:
> >>
> >> On Mon, 2 May 2022 at 14:23, Sébastien Bini <sebastien.bini_at_[hidden]> wrote:
> >>>
> >>> > A destructor may call arbitrary methods on the class under destruction,
> >>> > which will have no way to tell that the instance they are invoked on was
> >>> > previously relocated. How will you ensure safety in this scenario? It seems
> >>> > that there will be a considerable burden on the class author to ensure that
> >>> > all methods called from the destructor are safe to be called on a relocated
> >>> > instance, since the language will not ensure this, and a considerable
> >>> > maintenance burden going forward.
> >>>
> >>> I don't see how that's different from the destructor call on a moved instance.
> >>
> >>
> >> Here you're explicitly adding another state to the object for the (original and subsequent) authors to have to keep track of. Currently an object (e.g. a container) has: - default (empty) state; - value-containing state; - moved-from state (usually the same as empty state). Adding a relocated state means more work and more potential bugs. If instead relocation destroys the object then there are no additional states and quite often fewer,; the moved-from state can be made the same as the empty state, since there is no need for a singular moved-from state.
> >>
> >>> In fact the first version of the paper worked this way: the relocation destructor acted as a constructor for the new instance and as a destructor for the relocated instance. As such, the destructor of the relocated instance was not called, as the instance was already considered destructed.
> >>>
> >>> However, as others have pointed it out, this leads to an ABI break. Consider:
> >>>
> >>> void sink(T z);
> >>>
> >>> void fwd_to_sink(T y)
> >>> {
> >>> sink(reloc y); // oops
> >>> }
> >>>
> >>> void foo()
> >>> {
> >>> T x;
> >>> fwd_to_sink(reloc x);
> >>> }
> >>>
> >>> In fwd_to_sink, reloc cannot omit the call to the destructor of 'y', as it is called by 'foo'. For this to work:
> >>> - fwd_to_sink would need to somehow return some information on the destruction state of its parameters.
> >>> - or change the call convention so that the parameters are destructed in the called function body instead of the callee site.
> >>
> >>
> >> Yes, I recognize the issue. This can be resolved by stating that implementations may refuse to relocate function parameters, and providing a mechanism (e.g. an attribute) for the author to switch ABI to callee-destroy.
> >>
> >>> I thought of something similar:
> >>> struct T {
> >>> operator reloc(); // return a new instance of T, like a constructor would do
> >>> // or as a static variant with signature:
> >>> operator reloc(T&& self);
> >>> };
> >>>
> >>> But I found it very inconvenient to write the function body:
> >>>
> >>> struct T : public B {
> >>> operator reloc() {
> >>> // how to construct the B part of T into the new T using B's reloc?
> >>> // how to initialise the data-members of T using the reloc
> >>> }
> >>> }
> >>>
> >>> This can hardly reuse the constructor syntax (with base class and data-member initialisers). I couldn't find anything convincing in that path, but I can still give it more thought.
> >>
> >>
> >> Yes, this is a bit tricky (base classes in particular), though I have some ideas about possible syntaxes. However, much of the time `= default` will be sufficient, and in the remainder it may be acceptable to treat the source object as an xvalue, on the proviso that it will be immediately destructed. In other words I'm not convinced that this will be a problem in practice; some examples of classes that would need a user-provided relocation operation might help.
> >
> > --
> > Std-Proposals mailing list
> > Std-Proposals_at_[hidden]
> > https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals
> --
> Std-Proposals mailing list
> Std-Proposals_at_[hidden]
> https://lists.isocpp.org/mailman/listinfo.cgi/std-proposals

-- 
Magnus Fromreide     +46-13 17 68 48
Tornhagsvägen 24, 2tr					magfr_at_[hidden]
SE-582 37  LINKÖPING

Received on 2022-05-03 07:07:40