On Wed, 8 Jun 2022 at 09:34, Sébastien Bini <sebastien.bini@gmail.com> wrote:
Hi all,

Well, yes; certainly arguing about the exact spelling is bikeshedding. One aspect I think needs further exploration is a similar facility to allow running code before the relocation of subobjects begins, and I think constructor delegation could be the facility to solve that; the other aspect is symmetry with operator=(T).

The delegating constructor will not have the ability to do memberwise relocation and to avoid the destructor call. As such using the delegating constructor to inject code right before relocation will lose the benefit of having a relocation ctor in the first place.

Yes, this is a bit unfortunate. Possibly the delegated constructor could use std::relocate on subobjects and some trick found to prevent the destructor call. Or we could return to function-try-with-init.

Why would you need to have that option? We don't support that for any other ctor (custom code before member initialisation).

We do, via constructor delegation. The need here to invoke custom code is to safely relocate objects that both directly own a resource (i.e. their destructor releases the resource) and have data members with throwing relocate. The idea is that before starting to relocate members you can create a guard object that will release the resource if a data member relocator throws, to avoid a resource leak.

As for operator=(T), what do you have in mind? I am satisfied with leaving it implementation defined, users can freely use destruct + relocate or use swap at their own convenience.

Ah, hmm. The difficulty is that if we make it support `= default;` and that does the Obvious Thing of memberwise reloc-assign then we're setting up a footgun; it would turn into a memory leak for std::unique_ptr; memberwise reloc-assign is incorrect for resource-owning classes, even if memberwise reloc-construction is correct for them. But then again, a struct containing a non_null<unique_ptr> will need to generate a reloc-assignment operator that calls the reloc-assignment operator of non_null<unique_ptr> (since that's its only assignment operator), and it would feel weird to have a default implementation that the user can't access with `= default`. 

using N = non_null<unique_ptr<int>>;
struct S { N n; } s1 = ..., s2 = ...;
s1 = reloc s2; // calls into N::operator=(N); N::operator=(N const&) and N::operator=(N&&) are both deleted.

void sink(T);
void sink(T&&);

void foo(T val)
{
    sink(reloc val);
}

What happens now if T has opted-out of the ABI break with the non-standardized attribute? Before the reloc operator was in charge of building the new instance and could fallback to the move constructor. Now this is no longer the case as it depends on how the prvalue is consumed. Isn't it problematic as now the language needs to distinguish between prvalues whose destructor calls can be alleviated and the others? Which overload of sink is picked in that case?

The ABI break opt-out doesn't affect overloading, so sink(T) will be called in either case. Supposing T has the attribute applied, the move or copy constructor of T will be called to construct a temporary from val and this temporary is the argument to sink(T); this is much as would happen today, and is exactly the same if T does not have a candidate relocation operator/relocating constructor.

Another way to solve this is to state that `reloc obj` may turn the lvalue into a prvalue or an xvalue if obj is a function parameter. That choice would be implementation defined. As such, if obj had opted out of the ABI break then `reloc obj` would simply turn obj to an xvalue, no temporary would be created and sink(T&&) would be called. Otherwise, in the nominal case (obj is okay with the ABI break), then reloc turns obj into prvalue and sink(T) is called.

The problem is now that you have program structure dependent on an attribute, which is a no-no; if sink(T) turns out to be broken you won't find out until you remove the attribute, which goes against the ignorability principle. So I think the same overload sink(T) needs to be called in either case.