C++ Logo

std-proposals

Advanced search

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

From: Sébastien Bini <sebastien.bini_at_[hidden]>
Date: Thu, 5 May 2022 16:33:22 +0200
Hi all,

> That is, the destructor is called immediately, since the reloc operator
ended the scope of the name. I think that's a good thing.

I cannot be called immediately if the move ctor is called, as it could then
lead to objects being destructed twice:

void foo(T objA);
void fwd_to_foo(T objB) { foo(reloc objB); }

If reloc destructs objB then the caller of fwd_to_foo cannot know that it
was destructed (assuming there is no ABI change).

> 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.

Agreed.

> 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.)

Isn't it unusual to force different requirements (here on the reloc
operator) depending on whether the object to relocate is a function
parameter?
Also, If you cannot relocate function parameters, how would you relocate an
object inside an std::optional? You could still use 'emplace' but this more
or less forces you to wrap all relocatable types into an optional...

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.

As much as I would love to, I don't see how we can conciliate
relocation-only types with existing ABIs...

> 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.

Agreed.

> > 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.

rhs would be considered as destructed when control exits 'operator reloc'
function body. So its destructor is not called.

> 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.

I feel mixed about this idea. Developers may find it weird not to have a
reference somewhere. Besides I don't know how we could write base-or-member
initializers that would call their operator reloc member function (as if by
=default) if they all take parameters as value, not as reference. This
would imply that a copy is made somewhere, unless some new language rule
applies.

> 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.

Again, I am not sure how this would work if parameters are captured by
value (which would imply a copy is made somewhere).

> 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.

That would mean the function body cannot use the source parameter as it is
considered as destructed? It would be the first time a function is not
allowed to use its input parameter. But I agree it would feel safer as the
source object is logically destructed at this point (even though most of
the time it would be untouched).

> 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)

We do need a new syntax to call this special relocation constructor. The
main reason is for std::relocate_at implementation (which is needed for
container implementations). std::relocate_at(const T* src, T* dst) either
(a) makes memcpy if trivially relocatable, (b) constructs dst with some
kind of placement-new with the relocation ctor/dtor, or (c) placement-new
move ctor + dtor: new (dst) T{const_cast<T&&>(*src)}; src->~T();

In the first two revisions of my proposal, (b) was implemented by a call to
the relocation ctor directly: new (dst) T{const_cast<T~>(*src)}; That
worked well because of the relocation reference.

The syntax I suggested in my last email (T::T reloc(T&&)) allowed to write
the placement new: new (dst) T reloc{const_cast<T&&>(*src)};

If we find a good relocation constructor / operator reloc call syntax, then
we should get the base-or-member-initializer syntax for free.

Best regards,
Sébastien

On Wed, May 4, 2022 at 2:54 PM Edward Catmur <ecatmur_at_[hidden]> wrote:

> 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-05 14:33:34