Date: Mon, 10 Oct 2022 14:20:34 +0200
On Sun, Oct 9, 2022 at 6:35 PM Edward Catmur <ecatmur_at_[hidden]> wrote:
> On Fri, 7 Oct 2022 at 22:26, Edward Catmur <ecatmur_at_[hidden]> wrote:
>
>> On Fri, 7 Oct 2022 at 15:35, Sébastien Bini <sebastien.bini_at_[hidden]>
>> 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?
> On Fri, 7 Oct 2022 at 22:26, Edward Catmur <ecatmur_at_[hidden]> wrote:
>
>> On Fri, 7 Oct 2022 at 15:35, Sébastien Bini <sebastien.bini_at_[hidden]>
>> 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?
Received on 2022-10-10 12:20:46