C++ Logo

std-proposals

Advanced search

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

From: Edward Catmur <ecatmur_at_[hidden]>
Date: Fri, 20 May 2022 15:52:30 -0600
On Wed, 18 May 2022 at 05:56, Sébastien Bini <sebastien.bini_at_[hidden]>
wrote:

> > The wrinkle here is that for a relocator synthesized from move+destroy,
> the
> > destination and source lifetime do overlap, observably
>
> I am honestly confused here: what kind of synthesized relocation are you
> talking about? The reloc operator does not perform move+destruct. If the
> object is not trivially relocatable, and does not have an accessible
> operator reloc() member function, then all that reloc does is constructing
> a new object using the move (or copy) constructor, no destruction is
> involved by reloc per-se:
>

I'm talking about relocation as performed memberwise on base-and-members
under a relocation operation. In general, I prefer to regard relocation as
a single operation that can be accomplished in various different ways, and
that delayed destruction can be part of the operation; this is more
consistent than saying that operator reloc ends the lifetime of some
objects but not others.

A class that provides any user-defined copy constructor, move constructor,
> or destructor will have its operator reloc() implicitly deleted. Class
> authors are still free to default it or provide a definition.
>

Yes, this is fine.


> If a class has any of its base class or data-member with an implicitly
> deleted operator reloc(), then its operator reloc() will be implicitly
> deleted as well. And writing a custom operator reloc() may prove to be
> complicated. Consider:
>
> // B has an implicitly deleted operator reloc(), as it provides for
> instance a user-defined move constructor and destructor.
> struct T : B
> {
> D data;
>
> // User defined operator reloc()?
> operator reloc(T&& src) :
> B{std::move(src)}
> /* data is initialized using default rules */
> { src.~B(); /* manually destruct B part */ }
> }
>
> My take is that if T's operator reloc() were defaulted, then it would get
> deleted instead of having a default definition, as B's operator reloc() is
> implicitly deleted.
>

Oof, no. We do not want anyone to feel like they should manually destruct
subobjects. Acceptable ways to write this are:

    operator reloc(T&& src) :
        B{std::move(src)}
        /* data is initialized using default rules */
    {} // src::B is automatically destroyed

If a base-or-member has an initializer, then the corresponding
base-or-member on the source object is destroyed automatically - I'm not
sure exactly when, it could be immediately, or before entering the function
body, or after completing the function body.

But also, T should benefit from D's relocation operator, even if B is only
relocatable by move-and-destroy. So if T does not declare a relocation
operator, relocating T should be accomplished by move+destroy on its B base
subobject and relocate proper on it's D data member.

This is the same mechanism that applies to move constructors. AFAIK
> default-generated move-constructors do not delegate to their base class
> copy constructor if their base class move constructor is implicitly
> deleted. I am not sure it is a good idea to have operator reloc() make a
> synthesized relocation, calling move+destruct if the base class or
> data-member has an implicitly deleted operator reloc() member function, if
> that is what you are suggesting.
>

Yes, that's exactly what I'm suggesting. Composition is necessary - here
this means being able to combine a relocate-only and a move-destroy type as
data members of a class, and have that aggregate remain usable.

What problems do you foresee?


> > Any class that is trivially relocatable or has an accessible relocate
> > operator must either have been trivial already (in which case there is no
> > ABI impact) or have been explicitly opted in to relocation, by having a
> > relocator declared or defaulted. Since this is under the control of the
> > library author and/or user, I don't see it as an issue to say that
> > functions with parameters having trivial or user-defined relocate have a
> > different ABI (callee-destroy, or if necessary having automatic
> > accompanying flags in registers or on the stack). We accept that adding a
> > user-defined destructor, or virtual functions/bases, or changing data
> > members changes ABI, so why shouldn't defaulting/declaring a relocator?
>
> That's interesting, but I see some obstacles:
>
> operator reloc() is not necessarily opt-in. It may be implicitly declared
> and defined. This requires that the class has no user-defined constructors
> and destructors (among other things). A class may get an operator reloc()
> for free, without any changes in the class declaration nor in any of its
> base classes and data-members (recursively). Functions that will take such
> classes as a prvalue parameter will then have an ABI break?
> Under those circumstances the ABI break may not be necessary. Indeed such
> classes are POD types, and their destructor are a no-op. I guess we don't
> really care that their destructor call cannot be alleviated.
>

POD is a deprecated term; presumably you mean trivial? It is not observable
if a trivial class is relocated by move+destroy, so there is no ABI break
for these.


> For the other cases, operator reloc() is opt-in. However it is vital that
> most standard library classes support this operator reloc(). Most STL
> classes provide a user-defined constructor or destructor, and as such will
> get an implicitly deleted operator reloc(). If they don't default or
> provide their own operator reloc(), then any class that has an STL
> data-member will get its operator reloc() implicitly deleted as well. They
> will then struggle writing their own operator reloc() to support
> relocation, and will in addition be ruled out of trivial relocation (double
> pain).
>

I don't believe this is necessary. Whether the relocate operator is
suppressed should depend primarily on the class itself (on whether its
other special member functions are user-defined), not on its constituent
types. It is only only if a base or member is not relocatable by either
relocation proper or by fallback to move+destroy that the relocation
operator becomes non-defaultable (also if the class has a user-defined
destructor and some base or member has throwing relocate) (it may still be
possible to write a user-defined relocator, if a member has alternate
constructors, say).


> If the STL then explicitly supports operator reloc() (like it should be
> required) then any function having an STL parameter passed by value (like
> void foo(std::unique_ptr<T>); ) will get an ABI break. I really have no
> idea if that is acceptable.
>

Thiago has made an important and (to me) novel point regarding this; I'll
reply later.


> > We have a precedent for noexcept(true)-by-default and that's the
> destructor. I
> > think it could be acceptable to make the relocator obey the same rule
> because
> > after it's run -- and whether it has thrown or not -- one object's
> lifetime
> > has ended.
>
> Given that, AFAIK, there is no synthesized relocation involved, then yes
> the relocator (operator reloc() member function) can be noexcept(true) by
> default. Then any exception that leaks through a relocator causes
> std::terminate to be called.
>

I don't see enough benefit to noexcept(true) by default to justify the
potential loss of credibility.

Received on 2022-05-20 21:52:43