C++ Logo

std-proposals

Advanced search

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

From: Edward Catmur <ecatmur_at_[hidden]>
Date: Wed, 4 May 2022 13:54:18 +0100
On Tue, 3 May 2022 at 10:42, Sébastien Bini <sebastien.bini_at_[hidden]>
wrote:

> Hello all,
>
> I've been digesting yesterday discussions, and here is what I can propose
> to move forward:
>
> - types cannot be relocated if they don't have a copy constructor or a
> move constructor defined and accessible.
>
> I strongly believe that relocate-only types should be supported; it is
acceptable that they would not work as function parameters (the user can
wrap them in std::optional if necessary).

>
> - we remove relocation references and the relocation constructor.
> - we introduce a new "operator reloc(T&& rhs)" static member function
> in classes, which will act both as a constructor and a destructor (much
> like P0023 relocator). This function can be defaulted, implicitly declared
> and defined just like constructors. This would likely require new syntax
> bits, more on that later.
> - we keep the reloc operator. Calling reloc on an object will
> "relocate" the instance and mark the end of scope of its name. How the
> object is "relocated" is left to compiler vendors. The language offers the
> following relocation methods:
> - If the type is trivially relocatable (p1144), then using
> std::memcpy is authorized. If this method is used then the language
> enforces that the destructor of the relocated object is not called.
> - If the type provides an accessible "operator reloc" method,
> then it can be used to perform the relocation. Again the language enforces
> that the destructor of the relocated object is not called.
> - Simply use a regular move (std::move). The destructor of the
> moved instance is called normally (i.e. when it goes out of scope).
>
> The only case where the destructor needs to be called is function
arguments under caller-destroy ABI, so it is only necessary for the
behavior to be implementation-defined (i.e., possibly invoking move +
delayed destroy) in the case of function arguments. For automatic variables
it should be consistent to immediately relocate (trivially or user-defined)
with no destructor call, or copy/move + immediate destroy if no better
relocation operation is available.

Then, the copy/move constructor only needs to be accessible if a function
argument is relocated, not if the user is careful to only relocate
automatic variables. (It should be accessible even if the ABI is
callee-destroy, so that the correctness of code is not platform-dependent.)

I propose this function to have this signature "operator reloc(T&&)
> [noexcept]". noexcept shall be optional but throwing from such a function
> shall be an UB (just like it is an UB to throw from a destructor IIRC).


This is incorrect; it is allowed to throw from a destructor that is marked
noexcept(false). std::terminate is called (not UB, this is perfectly well
defined) if a noexcept(true) destructor (the default since C++11) throws,
or if an exception escapes a destructor invoked for stack unwind during
exception handling. So you can even throw from a destructor during stack
unwinding, as long as someone catches it before it hits the unwinder.

I agree that the relocator should be unconditionally noexcept, such that it
should not be possible to throw out from it (but std::terminate, not UB).
This is because the source object will have been partially relocated and
thus will be incapable of releasing resources.

I suggest the following initializer syntax for operator reloc:
>
> struct T : B {
> M data;
>
> // user-implementation equivalent to =default;
> operator reloc(T&& rhs) noexcept : B reloc{std::move(rhs)}, data
> reloc{std::move(rhs.data)} {}
> };
>

Would this call the destructor on the argument? In that case, it wouldn't
work for relocate-only bases and data members.

Also, it isn't clear from this syntax that rhs is going to be immediately
destroyed (or at least, for caller-destroy function arguments, that
accessing it between now and its eventual destructor call is forbidden);
since this is a totally new interface, you may as well take the argument by
value. Then also there is no way for user code to call operator reloc in a
dangerous manner.

@Edward Catmur: I read your suggestion about having automatic member
> initialisation as if defaulted, but I think it would contrast too much from
> constructors. Besides, users may have specific initialisation needs? We
> could still come up with something like 'operator reloc(T&& rhs) noexcept =
> bitcopies' (P1029) and still provide a function body to adjust self
> references...
>

 `= bitcopies` would be incorrect if a data member is not trivially
relocatable; we already have `= default` to indicate that a special member
function performs memberwise defaults and will be trivial if possible (i.e.
if memberwise trivial), so this should also work here. Then, `= default`
performing memberwise relocate indicates that should be the default if a
base or member initializer is omitted. Thus there's no need for new syntax
to indicate that a particular base-or-member is relocated. So your example
becomes simply:

operator reloc(T rhs) {} // equivalent to = default (other than not being
trivial)

If a base-or-member-initializer is provided, presumably that initializer
would be OK to access the corresponding and later subobjects on the source.
The corresponding subobject would not be relocated (unless you can suggest
syntax that would work for bases) but instead would be destructed
immediately after that base-or-member-initializer completes. Then by the
time the function body is entered the source object has been completely
destroyed (some bases or data members relocated, others specifically
destroyed) and its lifetime ended.

Received on 2022-05-04 12:54:30