C++ Logo

std-proposals

Advanced search

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

From: Edward Catmur <ecatmur_at_[hidden]>
Date: Mon, 10 Oct 2022 14:08:29 +0100
On Mon, 10 Oct 2022 at 13:20, Sébastien Bini <sebastien.bini_at_[hidden]>
wrote:

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

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

Received on 2022-10-10 13:08:41