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?
Oh, sorry - special member function. Copy/move/relocate constructors and assignment operators, and destructor; the Seven. I'm not counting other constructors (not even the default constructor) as those don't maintain invariants (though they may establish them) - a class without any of the seven SMFs can't claim to maintain any invariants, so std::decompose should be safe on it. The rules would be that when calling std::decompose on a prvalue of class type:
* if the class has any non-empty potentially overlapping direct subobjects (i.e., virtual bases or anonymous union members, but not EBO bases and [[no_unique_address]] members), then std::decompose is ill-formed
* otherwise, if it has any user-declared (not declared as defaulted) SMFs, or if it has any private direct subobjects, then std::decompose can only be called by members of the class and its friends
* otherwise, if it has any protected direct subobjects, std::decompose can only be called by members of the class, its friends, and its derived classes
* otherwise, std::decompose can be called from anywhere
And, wrt. structured binding of an object e of class type E,
* if std::tuple_size<E>::value is well-formed, the tuple-like protocol is invoked to perform recursive destructuring if e is prvalue and `get_all` is found, else binding;
* otherwise, if e is prvalue and neither E nor any of its base classes has any user-declared SMFs, data member destructuring is performed: the lifetime of e and its bases (but not the lifetime of its data members) is ended and the identifiers name the data members individually (as prvalue, if the data member is not ref-qualified)
* otherwise, binding to data members occurs as at present and the identifiers name the data members as xvalue or lvalue