On Sun, Oct 9, 2022 at 6:35 PM Edward Catmur <ecatmur@googlemail.com> wrote:
On Fri, 7 Oct 2022 at 22:26, Edward Catmur <ecatmur@googlemail.com> wrote:
On Fri, 7 Oct 2022 at 15:35, Sébastien Bini <sebastien.bini@gmail.com> wrote:
This is what we are doing when relocating a function parameter captured by value, if the function ABI is caller-detroy.
In both cases it would be ill-formed for relocate-only types.

Note that `auto reloc [x, y] = foo()` could also give the guarantee that `x` and `y` are individual objects (would be ill-formed otherwise). Hence C++ would not silently rule out the relocation ctor.
Not to say I am in favor of `auto reloc`, I think proper compiler error messages can do the job of explaining why the relocation ctor was ruled out.

Another thought - if `std::decompose` checks for private access to classes with their own SMFs (to guarantee that they have the right to bypass invariants), then writing instead `auto [x, y] = std::decompose<&S::x, &S::y>(foo());` will guarantee relocation at the cost of some extra verbosity.

Instead, consider: `std::decompose` is accessing (on behalf of its caller) each direct subobject (base and data member) that is returned. But it is also accessing the *other* direct subobjects that it does not return, in order to destroy them.  So let's say that to call std::decompose, you must have access to each direct subobject, including those that you don't request. (Plus their relocators or destructors, respectively.)

Then `auto [p] = std::decompose<&PainterWithGuard::_p>(reloc painterWithGuard);` would be ill-formed because in that context, `painterWithGuard._guard` is ill-formed.

Do you think this would work?

But if _guard were declared as public by mistake then all those safeties are bypassed and std::decompose will cause trouble.

In that case, aggregate-style structured binding would likely also work. And that's definitely the class author's fault.

Yes but it would not break things the way std::decompose does. (type may be movable but not decomposable).

True. I guess you have to accept some small risk of breakage; even an aggregate struct S { X x; Y y; }; could have invariants established (post-construction) between x and y that break if x is destroyed before y. But that's fragile code already; the author should have made those data members private. At least they can forestall this by adding a user-declared destructor.

Yes. I agree that we need this, but I would have liked it to be safer.

When I first proposed `get_bindings`, it took all subobjects of the class passed by prvalue. The language itself would split the source object, and pass all parts to get_bindings. This approach has several problems, it is quite inconvenient to use when we have a large amount of subobjects, and will not detect manual same-type data-member reordering in the class declaration. And it does not even support arrays. But at least, it was opt-in. You could only decompose an object if the type had implemented that weird get_bindings function, which would give the guarantee that this was a safe operation.

This is what is lacking to std::decompose IMO. The class has almost no say in this, it opts in by default. They can provide a user-defined destructor, but that feels like an opt-out side effect.
I'd prefer if things were reversed, a class type opts out by default (std::decompose is ill-formed) but can opt-in.

As I'm proposing it, if a class has *any* private immediate subobjects (direct base or member), then `std::decompose` can only be called by code within the class access boundary: that is, the class and its friends. 

So "opt-in by default" really only applies to aggregate types, and by making all their immediate subobjects public they're implicitly opting in to anything the Standard adds in future. Plus quite a lot of classes like that are likely subject to structured binding aggregate decomposition anyway, so it'd be odd to say that `auto [x, y] = S(...)` is allowed but `auto [x, y] = std::decompose<&S::x, &S::y>(S(...))` is not.

Another angle on the issue of user-defined destructors: how about if any class with a user-defined destructor (or, perhaps, any SMF) can only be `std::decompose`d by code within that class's access boundary, even if all its immediate subobjects are public? It'd be as if every class with a user-defined destructor has an implicit anonymous sizeless private member that must be accessible for `std::decompose` to be valid. This would protect classes that use SMFs to maintain invariants between public data members while allowing those classes to decompose themselves. It'd also mean that `std::unique_ptr::release(this unique_ptr)` can use std::decompose.

Sorry, I'm lost. What does SMF stand for? Could you clarify a bit more?